@unifyport/sdk-node 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -8
- package/README.zh-CN.md +306 -0
- package/package.json +20 -8
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,19 +1,320 @@
|
|
|
1
1
|
# `@unifyport/sdk-node`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`X-Api-Key` 保持固定认证边界以及统一的 timeout、错误和安全重试模型。Device API 内的 provider
|
|
5
|
-
账号管理、授权和运行态 operation 仍由该客户端提供。
|
|
3
|
+
[English](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/packages/sdk/README.md) | [简体中文](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/packages/sdk/README.zh-CN.md)
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
The UnifyPort Device API SDK for Node.js and TypeScript. It provides the type-safe
|
|
6
|
+
`UnifyPortDeviceClient`, consistent success response and error models, timeouts, safe retries,
|
|
7
|
+
cancellation, and cursor pagination.
|
|
8
|
+
|
|
9
|
+
The SDK uses a fixed `X-Api-Key` authentication boundary. Operations under `/v1/accounts/...` belong to
|
|
10
|
+
the Device API and are all available through `UnifyPortDeviceClient`.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
Every operation in the current public contract has a same-named SDK method. Major areas include:
|
|
15
|
+
|
|
16
|
+
- workspace and provider region queries;
|
|
17
|
+
- provider account resources, authorization flows, and runtime status;
|
|
18
|
+
- conversations, contacts, and groups;
|
|
19
|
+
- message sending and message actions;
|
|
20
|
+
- API key and webhook endpoint management.
|
|
21
|
+
|
|
22
|
+
Method names match their OpenAPI `operationId` values. See the
|
|
23
|
+
[API Reference](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/api-reference/README.md)
|
|
24
|
+
for each operation's parameters, request body, response fields, and TypeScript example. See
|
|
25
|
+
[API coverage](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/api-coverage.md) for
|
|
26
|
+
HTTP paths, retry behavior, and MCP policies.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js `>=22.12.0`
|
|
31
|
+
- A TypeScript project or a Node.js project with ESM support
|
|
32
|
+
|
|
33
|
+
This package provides only ESM `import` exports and TypeScript declarations. It does not provide a
|
|
34
|
+
CommonJS `require()` entry point.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
8
37
|
|
|
9
38
|
```bash
|
|
10
39
|
npm install @unifyport/sdk-node
|
|
11
40
|
```
|
|
12
41
|
|
|
13
|
-
|
|
14
|
-
[项目 README](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs#readme)。公开发布前必须运行:
|
|
42
|
+
Or with pnpm:
|
|
15
43
|
|
|
16
44
|
```bash
|
|
17
|
-
pnpm
|
|
18
|
-
|
|
45
|
+
pnpm add @unifyport/sdk-node
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { UnifyPortDeviceClient } from "@unifyport/sdk-node";
|
|
52
|
+
|
|
53
|
+
function requiredEnv(name: string): string {
|
|
54
|
+
const value = process.env[name];
|
|
55
|
+
if (!value) {
|
|
56
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read credentials only from the runtime environment so they never enter source or build artifacts.
|
|
62
|
+
const device = new UnifyPortDeviceClient({
|
|
63
|
+
baseUrl: requiredEnv("UNIFYPORT_DEVICE_API_BASE_URL"),
|
|
64
|
+
apiKey: requiredEnv("UNIFYPORT_DEVICE_API_KEY")
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await device.getWorkspace();
|
|
68
|
+
|
|
69
|
+
console.log(result.data.data.name);
|
|
70
|
+
console.log(result.status, result.requestId);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`ApiResult.data` contains the complete API JSON envelope, so resource data is usually available at
|
|
74
|
+
`result.data.data`.
|
|
75
|
+
|
|
76
|
+
## Client configuration
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const device = new UnifyPortDeviceClient({
|
|
80
|
+
baseUrl: "https://device.example.com",
|
|
81
|
+
apiKey: () => requiredEnv("UNIFYPORT_DEVICE_API_KEY"),
|
|
82
|
+
timeoutMs: 15_000,
|
|
83
|
+
maxResponseBytes: 4 * 1024 * 1024,
|
|
84
|
+
retry: {
|
|
85
|
+
maxRetries: 2,
|
|
86
|
+
baseDelayMs: 200,
|
|
87
|
+
maxDelayMs: 5_000
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Option | Required | Description |
|
|
93
|
+
| ------------------- | -------- | -------------------------------------------------------------------------------- |
|
|
94
|
+
| `baseUrl` | Yes | Absolute Device API URL; remote URLs must use HTTPS |
|
|
95
|
+
| `apiKey` | Yes | A string or a synchronous/asynchronous provider that returns a string |
|
|
96
|
+
| `fetch` | No | Custom `fetch` implementation with `(request: Request) => Promise<Response>` |
|
|
97
|
+
| `timeoutMs` | No | Defaults to `30000`; total timeout for each operation |
|
|
98
|
+
| `maxResponseBytes` | No | Defaults to 8 MiB, maximum 64 MiB; limits both wire body and normalized JSON |
|
|
99
|
+
| `retry` | No | Retry count and full-jitter backoff settings for safe operations |
|
|
100
|
+
| `allowInsecureHttp` | No | Defaults to `false`; allows only explicitly enabled loopback HTTP in development |
|
|
101
|
+
|
|
102
|
+
`baseUrl` must not contain a username, password, query, or fragment. The client injects the API key
|
|
103
|
+
centrally, and an individual operation cannot override the authentication header.
|
|
104
|
+
|
|
105
|
+
## Calling operations
|
|
106
|
+
|
|
107
|
+
Every method uses `(request?, execution?)`:
|
|
108
|
+
|
|
109
|
+
- Omit `request` for operations without parameters.
|
|
110
|
+
- Put path and query parameters in `request.params`.
|
|
111
|
+
- Put a JSON request body in `request.body`.
|
|
112
|
+
- Put per-call timeout, tighter retry settings, and the cancellation signal in `execution`.
|
|
113
|
+
|
|
114
|
+
### No-parameter reads
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const workspace = await device.getWorkspace();
|
|
118
|
+
console.log(workspace.data.data);
|
|
119
|
+
|
|
120
|
+
const accounts = await device.listAccounts();
|
|
121
|
+
for (const account of accounts.data.data) {
|
|
122
|
+
console.log(account.id, account.provider, account.status);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Path parameters
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const regions = await device.listProviderRegions({
|
|
130
|
+
params: {
|
|
131
|
+
path: { provider: "whatsapp" }
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
for (const region of regions.data.data.regions) {
|
|
136
|
+
console.log(region.region, region.supported, region.allocatable);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Path and query parameters
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const contacts = await device.listContacts({
|
|
144
|
+
params: {
|
|
145
|
+
path: { account_id: "acc_xxx" },
|
|
146
|
+
query: { limit: 100, q: "Alice" }
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log(contacts.data.data.items);
|
|
151
|
+
console.log(contacts.data.data.next_cursor);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### JSON request body
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const message = await device.sendMessage({
|
|
158
|
+
body: {
|
|
159
|
+
account_id: "acc_xxx",
|
|
160
|
+
to: { id: "recipient_xxx", type: "user" },
|
|
161
|
+
message: { type: "text", text: "Hello from UnifyPort" }
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log(message.data.data.message_id, message.data.data.status);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A write operation does not become retryable because of its HTTP method or caller configuration. Do not
|
|
169
|
+
write sensitive inputs such as message bodies to logs.
|
|
170
|
+
|
|
171
|
+
## Successful responses
|
|
172
|
+
|
|
173
|
+
Every operation returns `ApiResult<T>`:
|
|
174
|
+
|
|
175
|
+
| Field | Description |
|
|
176
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
177
|
+
| `data` | Complete API JSON envelope parsed according to the public contract |
|
|
178
|
+
| `status` | HTTP status |
|
|
179
|
+
| `requestId` | Optional server request ID |
|
|
180
|
+
| `response` | Underlying `Response` for headers and other successful transport metadata |
|
|
181
|
+
|
|
182
|
+
The SDK has already parsed the `response` body; do not depend on reading it again. Use the typed `data`
|
|
183
|
+
for application data.
|
|
184
|
+
|
|
185
|
+
## Timeouts, cancellation, and retries
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
|
|
190
|
+
const workspace = await device.getWorkspace(
|
|
191
|
+
{},
|
|
192
|
+
{
|
|
193
|
+
timeoutMs: 5_000,
|
|
194
|
+
retry: { maxRetries: 1 },
|
|
195
|
+
signal: controller.signal
|
|
196
|
+
}
|
|
197
|
+
);
|
|
19
198
|
```
|
|
199
|
+
|
|
200
|
+
A per-call `execution.retry` setting can only disable or tighten client-level retries:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
await device.getWorkspace({}, { retry: false });
|
|
204
|
+
await device.getWorkspace({}, { retry: { maxRetries: 0 } });
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Automatic retries apply only to safe operations explicitly allowed by policy. They handle network
|
|
208
|
+
failures and temporary statuses such as `408`, `429`, `502`, `503`, and `504`, and honor a valid
|
|
209
|
+
`Retry-After` value. Ordinary writes, account authorization submissions, and one-time secret operations
|
|
210
|
+
are not retried automatically.
|
|
211
|
+
|
|
212
|
+
## Cursor pagination
|
|
213
|
+
|
|
214
|
+
`paginateCursor` iterates page items and supports a maximum page count, repeated cursor detection, and
|
|
215
|
+
`AbortSignal`:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { paginateCursor } from "@unifyport/sdk-node";
|
|
219
|
+
|
|
220
|
+
const accountId = "acc_xxx";
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
|
|
223
|
+
for await (const contact of paginateCursor(
|
|
224
|
+
async (cursor, pageSignal) => {
|
|
225
|
+
// With exactOptionalPropertyTypes, do not assign undefined to an optional query field.
|
|
226
|
+
const query = cursor === undefined ? { limit: 100 } : { cursor, limit: 100 };
|
|
227
|
+
const result = await device.listContacts(
|
|
228
|
+
{
|
|
229
|
+
params: {
|
|
230
|
+
path: { account_id: accountId },
|
|
231
|
+
query
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
pageSignal === undefined ? {} : { signal: pageSignal }
|
|
235
|
+
);
|
|
236
|
+
return result.data.data;
|
|
237
|
+
},
|
|
238
|
+
{ maxPages: 100, signal: controller.signal }
|
|
239
|
+
)) {
|
|
240
|
+
console.log(contact.id, contact.display_name);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The pagination helper reads at most 10,000 pages by default. Application code should normally set a
|
|
245
|
+
smaller `maxPages` value appropriate to its workload.
|
|
246
|
+
|
|
247
|
+
## Error handling
|
|
248
|
+
|
|
249
|
+
All public errors extend `UnifyPortError`. Record only reviewed fields such as `status`, `code`,
|
|
250
|
+
`requestId`, `api`, and `operationId`. Do not serialize complete errors, requests, responses, or
|
|
251
|
+
headers.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { UnifyPortAbortError, UnifyPortApiError, UnifyPortTimeoutError } from "@unifyport/sdk-node";
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await device.getWorkspace();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof UnifyPortApiError) {
|
|
260
|
+
console.error({
|
|
261
|
+
status: error.status,
|
|
262
|
+
code: error.code,
|
|
263
|
+
requestId: error.requestId,
|
|
264
|
+
operationId: error.operationId
|
|
265
|
+
});
|
|
266
|
+
} else if (error instanceof UnifyPortTimeoutError) {
|
|
267
|
+
console.error({ timeoutMs: error.timeoutMs, operationId: error.operationId });
|
|
268
|
+
} else if (error instanceof UnifyPortAbortError) {
|
|
269
|
+
console.error({ aborted: true, operationId: error.operationId });
|
|
270
|
+
} else {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
| Error type | Scenario |
|
|
277
|
+
| ----------------------------- | --------------------------------------------------------------------- |
|
|
278
|
+
| `UnifyPortConfigurationError` | Invalid client configuration |
|
|
279
|
+
| `UnifyPortNetworkError` | Network or custom credential provider failure |
|
|
280
|
+
| `UnifyPortTimeoutError` | Operation timeout |
|
|
281
|
+
| `UnifyPortAbortError` | Caller cancellation through `AbortSignal` |
|
|
282
|
+
| `UnifyPortApiError` | API returned a non-success HTTP status |
|
|
283
|
+
| `UnifyPortResponseParseError` | Successful response could not be parsed safely |
|
|
284
|
+
| `UnifyPortPaginationError` | Missing/repeated cursor, page limit exceeded, or pagination cancelled |
|
|
285
|
+
|
|
286
|
+
## TypeScript types
|
|
287
|
+
|
|
288
|
+
The package root also exports the public contract types:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import type { DeviceApiComponents, DeviceApiOperations, DeviceApiPaths } from "@unifyport/sdk-node";
|
|
292
|
+
|
|
293
|
+
type Workspace = DeviceApiComponents["schemas"]["Workspace"];
|
|
294
|
+
type SendMessageOperation = DeviceApiOperations["sendMessage"];
|
|
295
|
+
type DevicePaths = DeviceApiPaths;
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
To avoid JavaScript precision loss, the SDK preserves integers outside the safe range as strings.
|
|
299
|
+
Fields modeled like `uint64` or `int64` may therefore be `number | string`. Do not call `Number(...)`
|
|
300
|
+
unconditionally unless the value's range is guaranteed.
|
|
301
|
+
|
|
302
|
+
## Security considerations
|
|
303
|
+
|
|
304
|
+
- Provide the API key through a secret manager or runtime environment; never hard-code or commit it.
|
|
305
|
+
- Require HTTPS in production. `allowInsecureHttp` accepts only explicitly enabled loopback addresses.
|
|
306
|
+
- Do not log authentication headers, authorization codes/passwords/sessions, message bodies, or
|
|
307
|
+
one-time secrets.
|
|
308
|
+
- Do not override `baseUrl` dynamically from external input. The client confines credentials to the
|
|
309
|
+
origin and path established during initialization.
|
|
310
|
+
- Design application-level idempotency and failure recovery for writes. Do not apply read-operation
|
|
311
|
+
retry assumptions to writes.
|
|
312
|
+
|
|
313
|
+
For more detail, see the
|
|
314
|
+
[security boundaries](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/security.md).
|
|
315
|
+
|
|
316
|
+
## Project and license
|
|
317
|
+
|
|
318
|
+
- [GitHub repository](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs)
|
|
319
|
+
- [Project README](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs#readme)
|
|
320
|
+
- [MIT License](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# `@unifyport/sdk-node`
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/packages/sdk/README.md) | [简体中文](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/packages/sdk/README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
面向 Node.js/TypeScript 的 UnifyPort Device API SDK。它提供类型安全的
|
|
6
|
+
`UnifyPortDeviceClient`、统一的成功响应与错误模型,以及 timeout、安全重试、取消和 cursor
|
|
7
|
+
分页能力。
|
|
8
|
+
|
|
9
|
+
SDK 使用固定的 `X-Api-Key` 认证边界。`/v1/accounts/...` operation 属于 Device API,统一由
|
|
10
|
+
`UnifyPortDeviceClient` 提供。
|
|
11
|
+
|
|
12
|
+
## 功能范围
|
|
13
|
+
|
|
14
|
+
当前公开契约中的全部 operation 都有同名 SDK 方法,主要覆盖:
|
|
15
|
+
|
|
16
|
+
- workspace 与 provider region 查询;
|
|
17
|
+
- provider 账号资源、授权流程与运行状态;
|
|
18
|
+
- 会话、联系人和群组;
|
|
19
|
+
- 消息发送与消息动作;
|
|
20
|
+
- API Key 与 webhook endpoint 管理。
|
|
21
|
+
|
|
22
|
+
方法名与 OpenAPI `operationId` 保持一致。每个 operation 的参数、请求体、返回字段和 TypeScript
|
|
23
|
+
示例见 [API Reference](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/zh-CN/api-reference/README.md);
|
|
24
|
+
HTTP path、重试和 MCP 策略见
|
|
25
|
+
[API 覆盖表](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/zh-CN/api-coverage.md)。
|
|
26
|
+
|
|
27
|
+
## 环境要求
|
|
28
|
+
|
|
29
|
+
- Node.js `>=22.12.0`
|
|
30
|
+
- TypeScript 项目或支持 ESM 的 Node.js 项目
|
|
31
|
+
|
|
32
|
+
该 package 只提供 ESM `import` 和 TypeScript 声明文件,不提供 CommonJS `require()` 入口。
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @unifyport/sdk-node
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
也可以使用 pnpm:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @unifyport/sdk-node
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 快速开始
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { UnifyPortDeviceClient } from "@unifyport/sdk-node";
|
|
50
|
+
|
|
51
|
+
function requiredEnv(name: string): string {
|
|
52
|
+
const value = process.env[name];
|
|
53
|
+
if (!value) {
|
|
54
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 凭据只从运行环境读取,避免进入源码、构建产物或版本控制。
|
|
60
|
+
const device = new UnifyPortDeviceClient({
|
|
61
|
+
baseUrl: requiredEnv("UNIFYPORT_DEVICE_API_BASE_URL"),
|
|
62
|
+
apiKey: requiredEnv("UNIFYPORT_DEVICE_API_KEY")
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await device.getWorkspace();
|
|
66
|
+
|
|
67
|
+
console.log(result.data.data.name);
|
|
68
|
+
console.log(result.status, result.requestId);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`ApiResult.data` 是完整 API JSON envelope,因此资源数据通常位于 `result.data.data`。
|
|
72
|
+
|
|
73
|
+
## Client 配置
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const device = new UnifyPortDeviceClient({
|
|
77
|
+
baseUrl: "https://device.example.com",
|
|
78
|
+
apiKey: () => requiredEnv("UNIFYPORT_DEVICE_API_KEY"),
|
|
79
|
+
timeoutMs: 15_000,
|
|
80
|
+
maxResponseBytes: 4 * 1024 * 1024,
|
|
81
|
+
retry: {
|
|
82
|
+
maxRetries: 2,
|
|
83
|
+
baseDelayMs: 200,
|
|
84
|
+
maxDelayMs: 5_000
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| 配置项 | 必填 | 说明 |
|
|
90
|
+
| ------------------- | ---- | --------------------------------------------------------------------- |
|
|
91
|
+
| `baseUrl` | 是 | Device API 的绝对 URL;远程地址必须使用 HTTPS |
|
|
92
|
+
| `apiKey` | 是 | 字符串,或返回字符串的同步/异步 provider |
|
|
93
|
+
| `fetch` | 否 | 自定义 `fetch` 实现,签名为 `(request: Request) => Promise<Response>` |
|
|
94
|
+
| `timeoutMs` | 否 | 默认 `30000`;每次 operation 的总超时 |
|
|
95
|
+
| `maxResponseBytes` | 否 | 默认 8 MiB,最大 64 MiB;同时限制 wire body 与规范化后的 JSON |
|
|
96
|
+
| `retry` | 否 | 安全 operation 的重试次数和 full-jitter 退避配置 |
|
|
97
|
+
| `allowInsecureHttp` | 否 | 默认 `false`;仅允许显式开启的 loopback HTTP 开发环境 |
|
|
98
|
+
|
|
99
|
+
`baseUrl` 不应包含 username、password、query 或 fragment。API key 由 client 统一注入,单次 operation
|
|
100
|
+
不能覆盖认证 header。
|
|
101
|
+
|
|
102
|
+
## 调用 operation
|
|
103
|
+
|
|
104
|
+
方法统一使用 `(request?, execution?)`:
|
|
105
|
+
|
|
106
|
+
- 没有参数的 operation 可以省略 `request`;
|
|
107
|
+
- path 和 query 参数放在 `request.params`;
|
|
108
|
+
- JSON 请求体放在 `request.body`;
|
|
109
|
+
- 单次 timeout、重试收紧和取消信号放在 `execution`。
|
|
110
|
+
|
|
111
|
+
### 无参数读取
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const workspace = await device.getWorkspace();
|
|
115
|
+
console.log(workspace.data.data);
|
|
116
|
+
|
|
117
|
+
const accounts = await device.listAccounts();
|
|
118
|
+
for (const account of accounts.data.data) {
|
|
119
|
+
console.log(account.id, account.provider, account.status);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Path 参数
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const regions = await device.listProviderRegions({
|
|
127
|
+
params: {
|
|
128
|
+
path: { provider: "whatsapp" }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const region of regions.data.data.regions) {
|
|
133
|
+
console.log(region.region, region.supported, region.allocatable);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Path 与 query 参数
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const contacts = await device.listContacts({
|
|
141
|
+
params: {
|
|
142
|
+
path: { account_id: "acc_xxx" },
|
|
143
|
+
query: { limit: 100, q: "Alice" }
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log(contacts.data.data.items);
|
|
148
|
+
console.log(contacts.data.data.next_cursor);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### JSON 请求体
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const message = await device.sendMessage({
|
|
155
|
+
body: {
|
|
156
|
+
account_id: "acc_xxx",
|
|
157
|
+
to: { id: "recipient_xxx", type: "user" },
|
|
158
|
+
message: { type: "text", text: "Hello from UnifyPort" }
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(message.data.data.message_id, message.data.data.status);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
写操作不会因为 HTTP method 或调用方配置而自动获得重试能力。消息正文等敏感输入不应写入日志。
|
|
166
|
+
|
|
167
|
+
## 成功响应
|
|
168
|
+
|
|
169
|
+
所有 operation 都返回 `ApiResult<T>`:
|
|
170
|
+
|
|
171
|
+
| 字段 | 说明 |
|
|
172
|
+
| ----------- | ------------------------------------------------------- |
|
|
173
|
+
| `data` | 按公开契约解析后的完整 API JSON envelope |
|
|
174
|
+
| `status` | HTTP status |
|
|
175
|
+
| `requestId` | 可选的服务端 request ID |
|
|
176
|
+
| `response` | 底层 `Response`,用于读取正常响应的 header 等传输元数据 |
|
|
177
|
+
|
|
178
|
+
`response` 的 body 已由 SDK 解析,不应依赖再次读取;业务数据使用类型化的 `data`。
|
|
179
|
+
|
|
180
|
+
## Timeout、取消与重试
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
|
|
185
|
+
const workspace = await device.getWorkspace(
|
|
186
|
+
{},
|
|
187
|
+
{
|
|
188
|
+
timeoutMs: 5_000,
|
|
189
|
+
retry: { maxRetries: 1 },
|
|
190
|
+
signal: controller.signal
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
单次 `execution.retry` 只能关闭或收紧 client 级重试:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await device.getWorkspace({}, { retry: false });
|
|
199
|
+
await device.getWorkspace({}, { retry: { maxRetries: 0 } });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
自动重试只适用于策略明确允许的安全 operation,处理网络失败和 `408`、`429`、`502`、`503`、
|
|
203
|
+
`504` 等临时状态,并遵守合法的 `Retry-After`。普通写操作、账号授权提交和一次性 secret operation
|
|
204
|
+
不会自动重试。
|
|
205
|
+
|
|
206
|
+
## Cursor 分页
|
|
207
|
+
|
|
208
|
+
`paginateCursor` 会逐项迭代分页数据,并提供最大页数、重复 cursor 检测与 `AbortSignal` 支持:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { paginateCursor } from "@unifyport/sdk-node";
|
|
212
|
+
|
|
213
|
+
const accountId = "acc_xxx";
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
|
|
216
|
+
for await (const contact of paginateCursor(
|
|
217
|
+
async (cursor, pageSignal) => {
|
|
218
|
+
// exactOptionalPropertyTypes 下不把 undefined 写入可选 query 字段。
|
|
219
|
+
const query = cursor === undefined ? { limit: 100 } : { cursor, limit: 100 };
|
|
220
|
+
const result = await device.listContacts(
|
|
221
|
+
{
|
|
222
|
+
params: {
|
|
223
|
+
path: { account_id: accountId },
|
|
224
|
+
query
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
pageSignal === undefined ? {} : { signal: pageSignal }
|
|
228
|
+
);
|
|
229
|
+
return result.data.data;
|
|
230
|
+
},
|
|
231
|
+
{ maxPages: 100, signal: controller.signal }
|
|
232
|
+
)) {
|
|
233
|
+
console.log(contact.id, contact.display_name);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
分页 helper 默认最多读取 10,000 页。业务代码通常应根据自身任务量设置更小的 `maxPages`。
|
|
238
|
+
|
|
239
|
+
## 错误处理
|
|
240
|
+
|
|
241
|
+
所有公开错误都继承 `UnifyPortError`。建议只记录经过筛选的 `status`、`code`、`requestId`、`api` 和
|
|
242
|
+
`operationId`,不要直接序列化完整错误、请求、响应或 header。
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { UnifyPortAbortError, UnifyPortApiError, UnifyPortTimeoutError } from "@unifyport/sdk-node";
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await device.getWorkspace();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof UnifyPortApiError) {
|
|
251
|
+
console.error({
|
|
252
|
+
status: error.status,
|
|
253
|
+
code: error.code,
|
|
254
|
+
requestId: error.requestId,
|
|
255
|
+
operationId: error.operationId
|
|
256
|
+
});
|
|
257
|
+
} else if (error instanceof UnifyPortTimeoutError) {
|
|
258
|
+
console.error({ timeoutMs: error.timeoutMs, operationId: error.operationId });
|
|
259
|
+
} else if (error instanceof UnifyPortAbortError) {
|
|
260
|
+
console.error({ aborted: true, operationId: error.operationId });
|
|
261
|
+
} else {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| 错误类型 | 场景 |
|
|
268
|
+
| ----------------------------- | --------------------------------------- |
|
|
269
|
+
| `UnifyPortConfigurationError` | client 配置无效 |
|
|
270
|
+
| `UnifyPortNetworkError` | 网络或自定义 credential provider 失败 |
|
|
271
|
+
| `UnifyPortTimeoutError` | operation 超时 |
|
|
272
|
+
| `UnifyPortAbortError` | 调用方通过 `AbortSignal` 取消 |
|
|
273
|
+
| `UnifyPortApiError` | API 返回非成功 HTTP status |
|
|
274
|
+
| `UnifyPortResponseParseError` | 成功响应无法安全解析 |
|
|
275
|
+
| `UnifyPortPaginationError` | cursor 缺失、重复、超出页数或分页被取消 |
|
|
276
|
+
|
|
277
|
+
## TypeScript 类型
|
|
278
|
+
|
|
279
|
+
根入口同时导出公开契约类型:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import type { DeviceApiComponents, DeviceApiOperations, DeviceApiPaths } from "@unifyport/sdk-node";
|
|
283
|
+
|
|
284
|
+
type Workspace = DeviceApiComponents["schemas"]["Workspace"];
|
|
285
|
+
type SendMessageOperation = DeviceApiOperations["sendMessage"];
|
|
286
|
+
type DevicePaths = DeviceApiPaths;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
为避免 JavaScript 精度丢失,SDK 会把超出安全范围的整数保留为字符串。因此 `uint64`/`int64` 风格
|
|
290
|
+
字段可能是 `number | string`;不要在没有范围保证时无条件调用 `Number(...)`。
|
|
291
|
+
|
|
292
|
+
## 安全注意事项
|
|
293
|
+
|
|
294
|
+
- API key 应从 secret manager 或运行环境提供,不要硬编码或提交到版本控制;
|
|
295
|
+
- 生产环境必须使用 HTTPS;`allowInsecureHttp` 只接受显式开启的 loopback 地址;
|
|
296
|
+
- 不要记录认证 header、授权 code/password/session、消息正文或一次性 secret;
|
|
297
|
+
- 不要从外部输入动态覆盖 `baseUrl`;client 会把凭据限制在初始化时确定的 origin/path;
|
|
298
|
+
- 对写操作自行设计业务幂等和失败恢复,不要把读取 operation 的重试假设套用到写入。
|
|
299
|
+
|
|
300
|
+
更多设计细节见[安全边界](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/docs/zh-CN/security.md)。
|
|
301
|
+
|
|
302
|
+
## 项目与许可证
|
|
303
|
+
|
|
304
|
+
- [GitHub 仓库](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs)
|
|
305
|
+
- [项目 README](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/README.zh-CN.md)
|
|
306
|
+
- [MIT License](https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifyport/sdk-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Type-safe Node.js SDK for the UnifyPort Device API.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Unify-Port/UnifyPort-SDK-Nodejs.git",
|
|
10
10
|
"directory": "packages/sdk"
|
|
11
11
|
},
|
|
12
|
+
"homepage": "https://github.com/Unify-Port/UnifyPort-SDK-Nodejs#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Unify-Port/UnifyPort-SDK-Nodejs/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"unifyport",
|
|
18
|
+
"sdk",
|
|
19
|
+
"device-api",
|
|
20
|
+
"nodejs",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
12
23
|
"publishConfig": {
|
|
13
24
|
"access": "public",
|
|
14
25
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -28,16 +39,17 @@
|
|
|
28
39
|
},
|
|
29
40
|
"files": [
|
|
30
41
|
"dist",
|
|
31
|
-
"README.md"
|
|
42
|
+
"README.md",
|
|
43
|
+
"README.zh-CN.md"
|
|
32
44
|
],
|
|
33
45
|
"sideEffects": false,
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"openapi-fetch": "0.17.0",
|
|
36
|
-
"openapi-typescript-helpers": "0.1.0"
|
|
37
|
-
},
|
|
38
46
|
"scripts": {
|
|
39
47
|
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
40
48
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
41
49
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"openapi-fetch": "0.17.0",
|
|
53
|
+
"openapi-typescript-helpers": "0.1.0"
|
|
42
54
|
}
|
|
43
|
-
}
|
|
55
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 UnifyPort
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|