tturn 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/LICENSE +21 -21
- package/README.en.md +4 -104
- package/README.md +136 -6
- package/README.zh-CN.md +120 -77
- package/dist/app.js +12 -7
- package/dist/cli.js +44 -9
- package/dist/credentials.js +12 -2
- package/dist/native-binding.d.ts +1 -1
- package/dist/native-binding.js +2 -2
- package/dist/service.d.ts +2 -1
- package/dist/service.js +20 -7
- package/dist/types.d.ts +7 -1
- package/index.node +0 -0
- package/package.json +50 -50
- package/tturn.win32-x64-msvc.node +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|
package/README.en.md
CHANGED
|
@@ -1,108 +1,8 @@
|
|
|
1
1
|
# Tturn (English)
|
|
2
2
|
|
|
3
|
-
[English](./README.
|
|
3
|
+
[English (default)](./README.md) | [中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
It uses an embedded native core (Rust + N-API), so you can install and run it directly from npm without Docker and without an external `turnserver.exe` runtime dependency.
|
|
5
|
+
The primary English documentation is now in `README.md`.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- Embedded TURN server for Node.js applications.
|
|
11
|
-
- Native data plane implemented in Rust for better throughput and lower overhead than pure JavaScript TURN implementations.
|
|
12
|
-
- Programmatic API and CLI are both provided.
|
|
13
|
-
- Time-limited TURN credentials are generated with HMAC-SHA1 (standard TURN REST style).
|
|
14
|
-
|
|
15
|
-
## 2) Current capabilities
|
|
16
|
-
|
|
17
|
-
- Start / stop TURN service from Node.
|
|
18
|
-
- Issue short-lived credentials.
|
|
19
|
-
- Return WebRTC ICE `urls/username/credential` directly.
|
|
20
|
-
- Health check (`running: boolean`).
|
|
21
|
-
|
|
22
|
-
## 3) Install
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npm i tturn
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## 4) Quick start (Node API)
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
import { Tturn } from "tturn";
|
|
32
|
-
|
|
33
|
-
const turn = new Tturn({
|
|
34
|
-
realm: "turn.example.com",
|
|
35
|
-
authSecret: "replace-with-your-secret",
|
|
36
|
-
publicIp: "1.2.3.4",
|
|
37
|
-
listenPort: 3478
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await turn.start();
|
|
41
|
-
|
|
42
|
-
const ice = turn.issueCredential({ ttlSec: 600, userId: "user-1001" });
|
|
43
|
-
console.log(ice);
|
|
44
|
-
|
|
45
|
-
// stop when exiting process
|
|
46
|
-
await turn.stop();
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## 5) CLI usage
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# Start embedded TURN service
|
|
53
|
-
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn start
|
|
54
|
-
|
|
55
|
-
# Print one ICE credential payload
|
|
56
|
-
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn credential
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## 6) API options
|
|
60
|
-
|
|
61
|
-
- `realm` (required): TURN realm/domain.
|
|
62
|
-
- `authSecret` (required): shared secret for dynamic credentials.
|
|
63
|
-
- `listenPort` (default `3478`): TURN listening port.
|
|
64
|
-
- `publicIp` (optional, recommended): public relay IP exposed to clients.
|
|
65
|
-
- `listeningIp` (default `0.0.0.0`): bind address.
|
|
66
|
-
- `minPort` / `maxPort`: reserved for relay port range control in next iterations.
|
|
67
|
-
|
|
68
|
-
## 7) Build from source
|
|
69
|
-
|
|
70
|
-
Requirements:
|
|
71
|
-
|
|
72
|
-
- Node.js >= 18
|
|
73
|
-
- Rust toolchain (stable)
|
|
74
|
-
|
|
75
|
-
Commands:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npm install
|
|
79
|
-
npm run build:native
|
|
80
|
-
./node_modules/.bin/tsc -p tsconfig.json
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Then run demo app:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
node dist/app.js
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Edit fixed config in `src/app.ts` before running.
|
|
90
|
-
|
|
91
|
-
## 8) Verify with Google WebRTC tool
|
|
92
|
-
|
|
93
|
-
1. Start service (`node dist/app.js`).
|
|
94
|
-
2. Copy printed `urls`, `username`, `credential`.
|
|
95
|
-
3. Open Google Trickle ICE tool and paste the ICE server config.
|
|
96
|
-
4. Gather candidates and confirm relay candidates are returned.
|
|
97
|
-
|
|
98
|
-
## 9) Publish to npm
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
npm login
|
|
102
|
-
npm publish --access public
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Notes:
|
|
106
|
-
|
|
107
|
-
- npm package names must be lowercase, so publish as `tturn`.
|
|
108
|
-
- For production users across multiple platforms, publish prebuilt `.node` artifacts for each target platform/arch.
|
|
7
|
+
- Main English doc: `README.md`
|
|
8
|
+
- Chinese doc: `README.zh-CN.md`
|
package/README.md
CHANGED
|
@@ -1,14 +1,144 @@
|
|
|
1
1
|
# Tturn
|
|
2
2
|
|
|
3
|
-
[English](./README.
|
|
3
|
+
[English (default)](./README.md) | [中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
`tturn` is a high-performance TURN server package for Node.js
|
|
5
|
+
`tturn` is a high-performance TURN server package for Node.js.
|
|
6
|
+
It embeds a native Rust core via N-API, so you can install from npm and run directly.
|
|
6
7
|
|
|
7
8
|
- No Docker runtime required.
|
|
8
9
|
- No external `turnserver.exe` runtime dependency.
|
|
9
|
-
- Programmatic API and CLI included.
|
|
10
|
+
- Programmatic API and CLI are both included.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## What this project provides
|
|
12
13
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
14
|
+
- Embedded TURN service for Node.js applications.
|
|
15
|
+
- Native data plane implemented in Rust.
|
|
16
|
+
- Dynamic TURN credentials using HMAC-SHA1 (TURN REST style).
|
|
17
|
+
- Direct ICE payload output (`urls`, `username`, `credential`).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i tturn
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start (Node API)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Tturn } from "tturn";
|
|
29
|
+
|
|
30
|
+
const turn = new Tturn({
|
|
31
|
+
realm: "turn.example.com",
|
|
32
|
+
password: "replace-with-your-password",
|
|
33
|
+
publicIp: "1.2.3.4",
|
|
34
|
+
listenPort: 3478,
|
|
35
|
+
username: "user-1001",
|
|
36
|
+
disableCredentialExpiry: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ice = await turn.start();
|
|
40
|
+
|
|
41
|
+
console.log(ice);
|
|
42
|
+
await turn.stop();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`start()` now returns one ICE payload directly, so bootstrap can be only `new Tturn(...)` + `start()`.
|
|
46
|
+
|
|
47
|
+
For long-running sessions, set `disableCredentialExpiry: true` to issue non-expiring credentials.
|
|
48
|
+
|
|
49
|
+
With static password mode (`password` + `username`), what you set is what clients use:
|
|
50
|
+
|
|
51
|
+
- output `username` is exactly your configured username
|
|
52
|
+
- output `credential` is exactly your configured password
|
|
53
|
+
|
|
54
|
+
## Credential options
|
|
55
|
+
|
|
56
|
+
`issueCredential(options)` supports:
|
|
57
|
+
|
|
58
|
+
- `ttlSec` (optional, default `3600`, minimum `60`)
|
|
59
|
+
- `username` (optional): pass a custom TURN username
|
|
60
|
+
- `userId` (optional): legacy prefix mode (`userId:expiresAt`)
|
|
61
|
+
|
|
62
|
+
Username behavior:
|
|
63
|
+
|
|
64
|
+
- If `username` already ends with a future unix timestamp (for example `alice:1730000000`), it is used as-is.
|
|
65
|
+
- Otherwise, `tturn` appends `:<expiresAt>` automatically so TURN auth remains valid.
|
|
66
|
+
- If both `username` and `userId` are provided, `username` takes priority.
|
|
67
|
+
|
|
68
|
+
## CLI usage
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Start embedded TURN service
|
|
72
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn start
|
|
73
|
+
|
|
74
|
+
# Print one ICE credential payload
|
|
75
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn credential
|
|
76
|
+
|
|
77
|
+
# Optional: provide custom username
|
|
78
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret TTURN_USERNAME=alice tturn credential
|
|
79
|
+
|
|
80
|
+
# Optional: disable credential expiry (long-lived credentials)
|
|
81
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret TTURN_USERNAME=alice TTURN_DISABLE_CREDENTIAL_EXPIRY=1 tturn credential
|
|
82
|
+
|
|
83
|
+
# Static account/password (exact value, no rewrite)
|
|
84
|
+
TURN_REALM=turn.example.com TURN_USERNAME=alice TURN_PASSWORD=alice-pass TTURN_DISABLE_CREDENTIAL_EXPIRY=1 tturn start
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Required env:
|
|
88
|
+
|
|
89
|
+
- `TURN_REALM`
|
|
90
|
+
- `TURN_SECRET` or `TURN_PASSWORD`
|
|
91
|
+
|
|
92
|
+
Optional env:
|
|
93
|
+
|
|
94
|
+
- `TURN_PUBLIC_IP`
|
|
95
|
+
- `TURN_PORT`
|
|
96
|
+
- `TTURN_TTL_SEC`
|
|
97
|
+
- `TTURN_USER_ID`
|
|
98
|
+
- `TTURN_USERNAME` (or `TURN_USERNAME`)
|
|
99
|
+
- `TTURN_DISABLE_CREDENTIAL_EXPIRY` (`1` or `true`)
|
|
100
|
+
|
|
101
|
+
## API options
|
|
102
|
+
|
|
103
|
+
- `realm` (required): TURN realm/domain.
|
|
104
|
+
- `authSecret` (optional): shared secret for dynamic credentials.
|
|
105
|
+
- `password` (optional): static TURN password (when set, returned credential stays fixed).
|
|
106
|
+
- `listenPort` (default `3478`): TURN listening port.
|
|
107
|
+
- `publicIp` (optional, recommended): public relay IP exposed to clients.
|
|
108
|
+
- `listeningIp` (default `0.0.0.0`): bind address.
|
|
109
|
+
- `username` / `userId` (optional): default credential username seed. `username` has higher priority.
|
|
110
|
+
- `ttlSec` (optional): default credential TTL used by `start()` and `issueCredential()`.
|
|
111
|
+
- `disableCredentialExpiry` (optional): disable timestamp expiry check and issue non-expiring credentials.
|
|
112
|
+
- `minPort` / `maxPort`: reserved for relay port range control in next iterations.
|
|
113
|
+
|
|
114
|
+
At least one of `authSecret` or `password` must be provided.
|
|
115
|
+
|
|
116
|
+
## Quick verify
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install
|
|
120
|
+
npm run build
|
|
121
|
+
|
|
122
|
+
TURN_REALM=turn.example.com TURN_PUBLIC_IP=1.2.3.4 TURN_USERNAME=alice TURN_PASSWORD=alice-pass TTURN_DISABLE_CREDENTIAL_EXPIRY=1 node dist/cli.js credential
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The returned JSON should keep `username = "alice"` and `credential = "alice-pass"`.
|
|
126
|
+
|
|
127
|
+
## Build from source
|
|
128
|
+
|
|
129
|
+
Requirements:
|
|
130
|
+
|
|
131
|
+
- Node.js >= 18
|
|
132
|
+
- Rust toolchain (stable)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm install
|
|
136
|
+
npm run build:native
|
|
137
|
+
npm run build:ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Run demo:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
node dist/app.js
|
|
144
|
+
```
|
package/README.zh-CN.md
CHANGED
|
@@ -1,107 +1,150 @@
|
|
|
1
|
-
# Tturn(中文)
|
|
2
|
-
|
|
3
|
-
[English](./README.
|
|
4
|
-
|
|
5
|
-
`tturn` 是一个高性能的 Node.js TURN 服务包。
|
|
6
|
-
它使用内嵌原生核心(Rust + N-API),可以直接通过 npm 安装并运行,不依赖 Docker,也不需要额外安装 `turnserver.exe` 作为运行时。
|
|
7
|
-
|
|
8
|
-
## 1)项目介绍
|
|
9
|
-
|
|
10
|
-
- `tturn` 是一个可直接在 Node.js 中运行的 TURN 服务包。
|
|
11
|
-
- 底层是 Rust + N-API 原生实现,性能和资源效率优于纯 JS TURN 实现。
|
|
12
|
-
- 不依赖 Docker,也不需要额外安装 `turnserver.exe` 作为运行时。
|
|
13
|
-
- 同时提供 Node API 和 CLI 两种使用方式。
|
|
14
|
-
|
|
15
|
-
## 2)当前功能
|
|
16
|
-
|
|
17
|
-
- 在 Node 中启动 / 停止 TURN 服务。
|
|
18
|
-
- 生成短时效 TURN 凭证(HMAC-SHA1)。
|
|
19
|
-
- 直接输出 WebRTC 所需的 ICE 参数(`urls/username/credential`)。
|
|
20
|
-
- 基础健康状态检查(`running: boolean`)。
|
|
21
|
-
|
|
22
|
-
## 3)安装
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npm i tturn
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## 4)Node API 快速使用
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
import { Tturn } from "tturn";
|
|
32
|
-
|
|
1
|
+
# Tturn(中文)
|
|
2
|
+
|
|
3
|
+
[English(主文档)](./README.md) | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`tturn` 是一个高性能的 Node.js TURN 服务包。
|
|
6
|
+
它使用内嵌原生核心(Rust + N-API),可以直接通过 npm 安装并运行,不依赖 Docker,也不需要额外安装 `turnserver.exe` 作为运行时。
|
|
7
|
+
|
|
8
|
+
## 1)项目介绍
|
|
9
|
+
|
|
10
|
+
- `tturn` 是一个可直接在 Node.js 中运行的 TURN 服务包。
|
|
11
|
+
- 底层是 Rust + N-API 原生实现,性能和资源效率优于纯 JS TURN 实现。
|
|
12
|
+
- 不依赖 Docker,也不需要额外安装 `turnserver.exe` 作为运行时。
|
|
13
|
+
- 同时提供 Node API 和 CLI 两种使用方式。
|
|
14
|
+
|
|
15
|
+
## 2)当前功能
|
|
16
|
+
|
|
17
|
+
- 在 Node 中启动 / 停止 TURN 服务。
|
|
18
|
+
- 生成短时效 TURN 凭证(HMAC-SHA1)。
|
|
19
|
+
- 直接输出 WebRTC 所需的 ICE 参数(`urls/username/credential`)。
|
|
20
|
+
- 基础健康状态检查(`running: boolean`)。
|
|
21
|
+
|
|
22
|
+
## 3)安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i tturn
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 4)Node API 快速使用
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Tturn } from "tturn";
|
|
32
|
+
|
|
33
33
|
const turn = new Tturn({
|
|
34
34
|
realm: "turn.example.com",
|
|
35
|
-
|
|
35
|
+
password: "replace-with-your-password",
|
|
36
36
|
publicIp: "1.2.3.4",
|
|
37
|
-
listenPort: 3478
|
|
37
|
+
listenPort: 3478,
|
|
38
|
+
username: "user-1001",
|
|
39
|
+
disableCredentialExpiry: true
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
await turn.start();
|
|
41
|
-
|
|
42
|
-
const ice = turn.issueCredential({ ttlSec: 600, userId: "user-1001" });
|
|
42
|
+
const ice = await turn.start();
|
|
43
43
|
console.log(ice);
|
|
44
44
|
|
|
45
45
|
await turn.stop();
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
`start()` 会直接返回一组 ICE,因此最简流程只需要 `new Tturn(...)` 和 `start()`。
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
# 启动内置 TURN 服务
|
|
52
|
-
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn start
|
|
50
|
+
如果你需要长时间持续连接,可设置 `disableCredentialExpiry: true`,生成不过期凭证。
|
|
53
51
|
|
|
52
|
+
在静态账号密码模式(`password` + `username`)下:
|
|
53
|
+
|
|
54
|
+
- 输出的 `username` 就是你配置的账号
|
|
55
|
+
- 输出的 `credential` 就是你配置的密码
|
|
56
|
+
|
|
57
|
+
## 5)CLI 用法
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 启动内置 TURN 服务
|
|
61
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn start
|
|
62
|
+
|
|
54
63
|
# 生成一组 ICE 凭证
|
|
55
64
|
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret tturn credential
|
|
65
|
+
|
|
66
|
+
# 可选:传入自定义 username
|
|
67
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret TTURN_USERNAME=alice tturn credential
|
|
68
|
+
|
|
69
|
+
# 可选:禁用凭证过期(长期凭证)
|
|
70
|
+
TURN_REALM=turn.example.com TURN_SECRET=replace-with-your-secret TTURN_USERNAME=alice TTURN_DISABLE_CREDENTIAL_EXPIRY=1 tturn credential
|
|
71
|
+
|
|
72
|
+
# 静态账号密码(原样输出,不改写)
|
|
73
|
+
TURN_REALM=turn.example.com TURN_USERNAME=alice TURN_PASSWORD=alice-pass TTURN_DISABLE_CREDENTIAL_EXPIRY=1 tturn start
|
|
56
74
|
```
|
|
57
75
|
|
|
58
|
-
##
|
|
76
|
+
## 5.1)凭证参数补充(username)
|
|
59
77
|
|
|
78
|
+
`issueCredential(options)` 额外支持 `username`(可选)。
|
|
79
|
+
|
|
80
|
+
- 如果 `username` 已经以未来时间戳结尾(例如 `alice:1730000000`),会原样使用。
|
|
81
|
+
- 否则会自动补上 `:<expiresAt>`,保证 TURN 鉴权可用。
|
|
82
|
+
- 同时传 `username` 和 `userId` 时,优先使用 `username`。
|
|
83
|
+
|
|
84
|
+
## 6)配置参数说明
|
|
85
|
+
|
|
60
86
|
- `realm`(必填):TURN realm / 域名。
|
|
61
|
-
- `authSecret
|
|
87
|
+
- `authSecret`(可选):动态凭证签名密钥。
|
|
88
|
+
- `password`(可选):静态 TURN 密码(设置后返回值保持固定)。
|
|
62
89
|
- `listenPort`(默认 `3478`):TURN 监听端口。
|
|
63
90
|
- `publicIp`(建议配置):客户端访问的公网 IP。
|
|
64
91
|
- `listeningIp`(默认 `0.0.0.0`):本地绑定地址。
|
|
92
|
+
- `username` / `userId`(可选):默认凭证用户名种子,`username` 优先级更高。
|
|
93
|
+
- `ttlSec`(可选):`start()` 与 `issueCredential()` 的默认凭证时效。
|
|
94
|
+
- `disableCredentialExpiry`(可选):禁用时间戳过期校验,生成不过期凭证。
|
|
65
95
|
- `minPort` / `maxPort`:预留给后续中继端口范围控制。
|
|
66
96
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
环境要求:
|
|
97
|
+
`authSecret` 和 `password` 至少需要提供一个。
|
|
70
98
|
|
|
71
|
-
|
|
72
|
-
- Rust 稳定版工具链
|
|
73
|
-
|
|
74
|
-
构建命令:
|
|
99
|
+
## 6.1)快速验证
|
|
75
100
|
|
|
76
101
|
```bash
|
|
77
102
|
npm install
|
|
78
|
-
npm run build
|
|
79
|
-
./node_modules/.bin/tsc -p tsconfig.json
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
运行示例:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
node dist/app.js
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
运行前请先修改 `src/app.ts` 中的固定配置项。
|
|
89
|
-
|
|
90
|
-
## 8)使用 Google WebRTC 工具验证
|
|
103
|
+
npm run build
|
|
91
104
|
|
|
92
|
-
1.
|
|
93
|
-
2. 复制控制台打印的 `urls`、`username`、`credential`
|
|
94
|
-
3. 打开 Google Trickle ICE 页面,填入 ICE Server
|
|
95
|
-
4. 执行采集并确认出现 relay candidate
|
|
96
|
-
|
|
97
|
-
## 9)发布到 npm
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npm login
|
|
101
|
-
npm publish --access public
|
|
105
|
+
TURN_REALM=turn.example.com TURN_PUBLIC_IP=1.2.3.4 TURN_USERNAME=alice TURN_PASSWORD=alice-pass TTURN_DISABLE_CREDENTIAL_EXPIRY=1 node dist/cli.js credential
|
|
102
106
|
```
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
返回 JSON 中应保持 `username = "alice"`、`credential = "alice-pass"`。
|
|
109
|
+
|
|
110
|
+
## 7)源码构建
|
|
111
|
+
|
|
112
|
+
环境要求:
|
|
113
|
+
|
|
114
|
+
- Node.js >= 18
|
|
115
|
+
- Rust 稳定版工具链
|
|
116
|
+
|
|
117
|
+
构建命令:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install
|
|
121
|
+
npm run build:native
|
|
122
|
+
./node_modules/.bin/tsc -p tsconfig.json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
运行示例:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
node dist/app.js
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
运行前请先修改 `src/app.ts` 中的固定配置项。
|
|
132
|
+
|
|
133
|
+
## 8)使用 Google WebRTC 工具验证
|
|
134
|
+
|
|
135
|
+
1. 启动服务:`node dist/app.js`
|
|
136
|
+
2. 复制控制台打印的 `urls`、`username`、`credential`
|
|
137
|
+
3. 打开 Google Trickle ICE 页面,填入 ICE Server
|
|
138
|
+
4. 执行采集并确认出现 relay candidate
|
|
139
|
+
|
|
140
|
+
## 9)发布到 npm
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm login
|
|
144
|
+
npm publish --access public
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
说明:
|
|
148
|
+
|
|
149
|
+
- npm 包名必须小写,所以发布名为 `tturn`。
|
|
150
|
+
- 生产环境建议按平台预编译并发布对应 `.node` 二进制文件。
|
package/dist/app.js
CHANGED
|
@@ -3,20 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const index_1 = require("./index");
|
|
4
4
|
const APP_CONFIG = {
|
|
5
5
|
realm: "turn.example.com",
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
publicIp: "192.168.3.66",
|
|
7
|
+
listenPort: 22224,
|
|
8
|
+
username: "metadigiee",
|
|
9
|
+
password: "22222",
|
|
10
|
+
ttlSec: 600,
|
|
11
|
+
disableCredentialExpiry: true
|
|
10
12
|
};
|
|
11
13
|
async function main() {
|
|
12
14
|
const service = new index_1.Tturn({
|
|
13
15
|
realm: APP_CONFIG.realm,
|
|
14
16
|
authSecret: APP_CONFIG.authSecret,
|
|
17
|
+
password: APP_CONFIG.password,
|
|
15
18
|
publicIp: APP_CONFIG.publicIp,
|
|
16
|
-
listenPort: APP_CONFIG.listenPort
|
|
19
|
+
listenPort: APP_CONFIG.listenPort,
|
|
20
|
+
username: APP_CONFIG.username,
|
|
21
|
+
ttlSec: APP_CONFIG.ttlSec,
|
|
22
|
+
disableCredentialExpiry: APP_CONFIG.disableCredentialExpiry
|
|
17
23
|
});
|
|
18
|
-
await service.start();
|
|
19
|
-
const ice = service.issueCredential({ ttlSec: APP_CONFIG.ttlSec, userId: "google-test" });
|
|
24
|
+
const ice = await service.start();
|
|
20
25
|
console.log("[tturn] started.");
|
|
21
26
|
console.log("[tturn] use this ICE server in Google WebRTC tool:");
|
|
22
27
|
console.log(JSON.stringify(ice, null, 2));
|
package/dist/cli.js
CHANGED
|
@@ -5,27 +5,40 @@ const index_1 = require("./index");
|
|
|
5
5
|
async function run() {
|
|
6
6
|
const command = process.argv[2];
|
|
7
7
|
if (command === "credential") {
|
|
8
|
-
const secret = mustGetEnv("TURN_SECRET");
|
|
9
8
|
const realm = mustGetEnv("TURN_REALM");
|
|
10
|
-
const
|
|
9
|
+
const authOptions = resolveAuthOptions();
|
|
10
|
+
const service = (0, index_1.createTurnService)({
|
|
11
|
+
realm,
|
|
12
|
+
authSecret: authOptions.authSecret,
|
|
13
|
+
password: authOptions.password,
|
|
14
|
+
publicIp: process.env.TURN_PUBLIC_IP,
|
|
15
|
+
disableCredentialExpiry: readBoolEnv("TTURN_DISABLE_CREDENTIAL_EXPIRY") ?? Boolean(authOptions.password)
|
|
16
|
+
});
|
|
11
17
|
const ice = service.issueCredential({
|
|
12
18
|
ttlSec: process.env.TTURN_TTL_SEC ? Number(process.env.TTURN_TTL_SEC) : 3600,
|
|
13
|
-
userId: process.env.TTURN_USER_ID
|
|
19
|
+
userId: process.env.TTURN_USER_ID,
|
|
20
|
+
username: readUsernameEnv()
|
|
14
21
|
});
|
|
15
22
|
process.stdout.write(`${JSON.stringify(ice, null, 2)}\n`);
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
18
25
|
if (command === "start") {
|
|
19
|
-
const secret = mustGetEnv("TURN_SECRET");
|
|
20
26
|
const realm = mustGetEnv("TURN_REALM");
|
|
27
|
+
const authOptions = resolveAuthOptions();
|
|
21
28
|
const service = (0, index_1.createTurnService)({
|
|
22
29
|
realm,
|
|
23
|
-
authSecret:
|
|
30
|
+
authSecret: authOptions.authSecret,
|
|
31
|
+
password: authOptions.password,
|
|
24
32
|
publicIp: process.env.TURN_PUBLIC_IP,
|
|
25
|
-
listenPort: process.env.TURN_PORT ? Number(process.env.TURN_PORT) : 3478
|
|
33
|
+
listenPort: process.env.TURN_PORT ? Number(process.env.TURN_PORT) : 3478,
|
|
34
|
+
ttlSec: process.env.TTURN_TTL_SEC ? Number(process.env.TTURN_TTL_SEC) : 3600,
|
|
35
|
+
username: readUsernameEnv(),
|
|
36
|
+
userId: process.env.TTURN_USER_ID,
|
|
37
|
+
disableCredentialExpiry: readBoolEnv("TTURN_DISABLE_CREDENTIAL_EXPIRY") ?? Boolean(authOptions.password)
|
|
26
38
|
});
|
|
27
|
-
await service.start();
|
|
39
|
+
const ice = await service.start();
|
|
28
40
|
process.stdout.write("tturn started\n");
|
|
41
|
+
process.stdout.write(`${JSON.stringify(ice, null, 2)}\n`);
|
|
29
42
|
await waitForSignal();
|
|
30
43
|
await service.stop();
|
|
31
44
|
process.stdout.write("tturn stopped\n");
|
|
@@ -48,11 +61,33 @@ function printUsage() {
|
|
|
48
61
|
" tturn credential # prints one ICE server credential JSON",
|
|
49
62
|
"",
|
|
50
63
|
"required env:",
|
|
51
|
-
" TURN_REALM
|
|
64
|
+
" TURN_REALM",
|
|
65
|
+
" TURN_SECRET or TURN_PASSWORD",
|
|
52
66
|
"optional env:",
|
|
53
|
-
" TURN_PUBLIC_IP, TURN_PORT, TTURN_TTL_SEC, TTURN_USER_ID"
|
|
67
|
+
" TURN_PUBLIC_IP, TURN_PORT, TTURN_TTL_SEC, TTURN_USER_ID, TTURN_USERNAME (or TURN_USERNAME), TTURN_DISABLE_CREDENTIAL_EXPIRY"
|
|
54
68
|
].join("\n") + "\n");
|
|
55
69
|
}
|
|
70
|
+
function readUsernameEnv() {
|
|
71
|
+
return process.env.TTURN_USERNAME ?? process.env.TURN_USERNAME;
|
|
72
|
+
}
|
|
73
|
+
function resolveAuthOptions() {
|
|
74
|
+
const authSecret = process.env.TURN_SECRET;
|
|
75
|
+
const password = process.env.TURN_PASSWORD;
|
|
76
|
+
if (!authSecret && !password) {
|
|
77
|
+
throw new Error("Missing required env: TURN_SECRET or TURN_PASSWORD");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
authSecret: authSecret ?? "",
|
|
81
|
+
password
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readBoolEnv(name) {
|
|
85
|
+
const value = process.env[name];
|
|
86
|
+
if (!value) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
90
|
+
}
|
|
56
91
|
function waitForSignal() {
|
|
57
92
|
return new Promise((resolve) => {
|
|
58
93
|
const keepAlive = setInterval(() => {
|
package/dist/credentials.js
CHANGED
|
@@ -6,8 +6,18 @@ const DEFAULT_TTL_SEC = 3600;
|
|
|
6
6
|
function createTurnCredential(authSecret, options = {}) {
|
|
7
7
|
const ttlSec = Math.max(60, options.ttlSec ?? DEFAULT_TTL_SEC);
|
|
8
8
|
const expiresAt = Math.floor(Date.now() / 1000) + ttlSec;
|
|
9
|
-
const
|
|
10
|
-
const username = `${userPrefix}${expiresAt}`;
|
|
9
|
+
const username = resolveUsername(options, expiresAt);
|
|
11
10
|
const password = (0, node_crypto_1.createHmac)("sha1", authSecret).update(username).digest("base64");
|
|
12
11
|
return { username, password, ttlSec, expiresAt };
|
|
13
12
|
}
|
|
13
|
+
function resolveUsername(options, expiresAt) {
|
|
14
|
+
if (options.username) {
|
|
15
|
+
const tail = options.username.split(":").pop();
|
|
16
|
+
if (tail && /^\d+$/.test(tail) && Number(tail) > Math.floor(Date.now() / 1000)) {
|
|
17
|
+
return options.username;
|
|
18
|
+
}
|
|
19
|
+
return `${options.username}:${expiresAt}`;
|
|
20
|
+
}
|
|
21
|
+
const userPrefix = options.userId ? `${options.userId}:` : "";
|
|
22
|
+
return `${userPrefix}${expiresAt}`;
|
|
23
|
+
}
|
package/dist/native-binding.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ interface NativeCredential {
|
|
|
8
8
|
interface NativeTurnService {
|
|
9
9
|
start(detached?: boolean): void;
|
|
10
10
|
stop(): void;
|
|
11
|
-
issueCredential(ttlSec?: number, userId?: string): NativeCredential;
|
|
11
|
+
issueCredential(ttlSec?: number, userId?: string, username?: string): NativeCredential;
|
|
12
12
|
getIceUrls(): string[];
|
|
13
13
|
health(): {
|
|
14
14
|
running: boolean;
|
package/dist/native-binding.js
CHANGED
|
@@ -6,9 +6,9 @@ const node_path_1 = require("node:path");
|
|
|
6
6
|
function loadNativeBinding() {
|
|
7
7
|
const root = (0, node_path_1.join)(__dirname, "..");
|
|
8
8
|
const candidates = [
|
|
9
|
+
(0, node_path_1.join)(root, binaryName()),
|
|
9
10
|
(0, node_path_1.join)(root, "index.node"),
|
|
10
|
-
(0, node_path_1.join)(root, "native", "index.node")
|
|
11
|
-
(0, node_path_1.join)(root, binaryName())
|
|
11
|
+
(0, node_path_1.join)(root, "native", "index.node")
|
|
12
12
|
];
|
|
13
13
|
for (const filePath of candidates) {
|
|
14
14
|
if (!(0, node_fs_1.existsSync)(filePath)) {
|
package/dist/service.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { IceServer, IssueCredentialOptions, StartOptions, TurnServiceOptions } from "./types";
|
|
2
2
|
export declare class TurnService {
|
|
3
3
|
private readonly native;
|
|
4
|
+
private readonly defaultIssueOptions;
|
|
4
5
|
constructor(options: TurnServiceOptions);
|
|
5
|
-
start(startOptions?: StartOptions): Promise<
|
|
6
|
+
start(startOptions?: StartOptions): Promise<IceServer>;
|
|
6
7
|
stop(): Promise<void>;
|
|
7
8
|
issueCredential(issueOptions?: IssueCredentialOptions): IceServer;
|
|
8
9
|
getIceUrls(): string[];
|
package/dist/service.js
CHANGED
|
@@ -4,23 +4,36 @@ exports.TurnService = void 0;
|
|
|
4
4
|
const native_binding_1 = require("./native-binding");
|
|
5
5
|
class TurnService {
|
|
6
6
|
constructor(options) {
|
|
7
|
+
const { username, password, userId, ttlSec, ...nativeOptions } = options;
|
|
8
|
+
const disableCredentialExpiry = options.disableCredentialExpiry ?? Boolean(password);
|
|
7
9
|
this.native = new native_binding_1.binding.NativeTurnService({
|
|
8
|
-
...
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
...nativeOptions,
|
|
11
|
+
authSecret: nativeOptions.authSecret ?? "",
|
|
12
|
+
listenPort: nativeOptions.listenPort ?? 3478,
|
|
13
|
+
minPort: nativeOptions.minPort ?? 49152,
|
|
14
|
+
maxPort: nativeOptions.maxPort ?? 65535,
|
|
15
|
+
publicIp: nativeOptions.publicIp ?? nativeOptions.realm,
|
|
16
|
+
listeningIp: nativeOptions.listeningIp ?? "0.0.0.0",
|
|
17
|
+
username,
|
|
18
|
+
password,
|
|
19
|
+
disableCredentialExpiry
|
|
14
20
|
});
|
|
21
|
+
this.defaultIssueOptions = {
|
|
22
|
+
ttlSec,
|
|
23
|
+
username,
|
|
24
|
+
userId
|
|
25
|
+
};
|
|
15
26
|
}
|
|
16
27
|
async start(startOptions = {}) {
|
|
17
28
|
this.native.start(Boolean(startOptions.detached));
|
|
29
|
+
return this.issueCredential();
|
|
18
30
|
}
|
|
19
31
|
async stop() {
|
|
20
32
|
this.native.stop();
|
|
21
33
|
}
|
|
22
34
|
issueCredential(issueOptions = {}) {
|
|
23
|
-
const
|
|
35
|
+
const merged = { ...this.defaultIssueOptions, ...issueOptions };
|
|
36
|
+
const out = this.native.issueCredential(merged.ttlSec, merged.userId, merged.username);
|
|
24
37
|
return {
|
|
25
38
|
urls: this.native.getIceUrls(),
|
|
26
39
|
username: out.username,
|
package/dist/types.d.ts
CHANGED
|
@@ -11,16 +11,22 @@ export interface IceServer {
|
|
|
11
11
|
}
|
|
12
12
|
export interface IssueCredentialOptions {
|
|
13
13
|
ttlSec?: number;
|
|
14
|
+
username?: string;
|
|
14
15
|
userId?: string;
|
|
15
16
|
}
|
|
16
17
|
export interface TurnServiceOptions {
|
|
17
18
|
realm: string;
|
|
18
|
-
authSecret
|
|
19
|
+
authSecret?: string;
|
|
19
20
|
listenPort?: number;
|
|
20
21
|
minPort?: number;
|
|
21
22
|
maxPort?: number;
|
|
22
23
|
publicIp?: string;
|
|
23
24
|
listeningIp?: string;
|
|
25
|
+
username?: string;
|
|
26
|
+
password?: string;
|
|
27
|
+
userId?: string;
|
|
28
|
+
ttlSec?: number;
|
|
29
|
+
disableCredentialExpiry?: boolean;
|
|
24
30
|
}
|
|
25
31
|
export interface StartOptions {
|
|
26
32
|
detached?: boolean;
|
package/index.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tturn",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "High-performance TURN server for Node.js with embedded native core",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"turn",
|
|
7
|
-
"webrtc",
|
|
8
|
-
"napi",
|
|
9
|
-
"rust",
|
|
10
|
-
"stun",
|
|
11
|
-
"relay"
|
|
12
|
-
],
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"author": "",
|
|
15
|
-
"homepage": "https://github.com/txzh007/ttrun",
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "https://github.com/txzh007/ttrun.git"
|
|
19
|
-
},
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/txzh007/ttrun/issues"
|
|
22
|
-
},
|
|
23
|
-
"main": "dist/index.js",
|
|
24
|
-
"types": "dist/index.d.ts",
|
|
25
|
-
"bin": {
|
|
26
|
-
"tturn": "dist/cli.js"
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"dist",
|
|
30
|
-
"index.node",
|
|
31
|
-
"tturn.*.node",
|
|
32
|
-
"README.md",
|
|
33
|
-
"README.en.md",
|
|
34
|
-
"README.zh-CN.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=18"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build:native": "node scripts/build-native.js",
|
|
42
|
-
"build": "npm run build:native && tsc -p tsconfig.json",
|
|
43
|
-
"build:ts": "tsc -p tsconfig.json"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {},
|
|
46
|
-
"devDependencies": {
|
|
47
|
-
"@types/node": "^22.13.10",
|
|
48
|
-
"typescript": "^5.8.2"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tturn",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "High-performance TURN server for Node.js with embedded native core",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"turn",
|
|
7
|
+
"webrtc",
|
|
8
|
+
"napi",
|
|
9
|
+
"rust",
|
|
10
|
+
"stun",
|
|
11
|
+
"relay"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "",
|
|
15
|
+
"homepage": "https://github.com/txzh007/ttrun",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/txzh007/ttrun.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/txzh007/ttrun/issues"
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"bin": {
|
|
26
|
+
"tturn": "dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"index.node",
|
|
31
|
+
"tturn.*.node",
|
|
32
|
+
"README.md",
|
|
33
|
+
"README.en.md",
|
|
34
|
+
"README.zh-CN.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build:native": "node scripts/build-native.js",
|
|
42
|
+
"build": "npm run build:native && tsc -p tsconfig.json",
|
|
43
|
+
"build:ts": "tsc -p tsconfig.json"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.13.10",
|
|
48
|
+
"typescript": "^5.8.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
Binary file
|