@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 CHANGED
@@ -1,19 +1,320 @@
1
1
  # `@unifyport/sdk-node`
2
2
 
3
- 面向 Node.js/TypeScript 的 UnifyPort Device API SDK,提供 `UnifyPortDeviceClient`,并通过
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 check
18
- pnpm public:check
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)
@@ -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.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
- "description": "面向 Node.js 的 UnifyPort Device API 类型安全 SDK。",
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.