@skytalesh/sdk 0.4.0
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 +337 -0
- package/js/api.d.ts +50 -0
- package/js/api.js +181 -0
- package/js/channel-manager.d.ts +195 -0
- package/js/channel-manager.js +507 -0
- package/js/envelope.d.ts +71 -0
- package/js/envelope.js +117 -0
- package/js/errors.d.ts +77 -0
- package/js/errors.js +106 -0
- package/js/index.d.ts +19 -0
- package/js/index.js +35 -0
- package/js/mcp-transport.d.ts +96 -0
- package/js/mcp-transport.js +144 -0
- package/package.json +51 -0
- package/skytale-sdk.darwin-arm64.node +0 -0
- package/skytale-sdk.darwin-x64.node +0 -0
- package/skytale-sdk.linux-arm64-gnu.node +0 -0
- package/skytale-sdk.linux-x64-gnu.node +0 -0
- package/skytale-sdk.win32-x64-msvc.node +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# @skytalesh/sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@skytalesh/sdk)
|
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
5
|
+
|
|
6
|
+
TypeScript/Node.js SDK for Skytale — encrypted channels for AI agents.
|
|
7
|
+
|
|
8
|
+
Create MLS-encrypted communication channels with multi-protocol support (SLIM, A2A, ACP, MCP, ANP, LMOS, NLIP). Native Rust core via NAPI for performance.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @skytalesh/sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Prebuilt binaries are available for macOS (x64, ARM64), Linux (x64, ARM64), and Windows (x64). Requires Node.js 18+.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Get an API key with the Skytale CLI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
skytale signup you@example.com
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or set the key directly:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export SKYTALE_API_KEY="sk_live_..."
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { SkytaleChannelManager } from '@skytalesh/sdk';
|
|
36
|
+
|
|
37
|
+
const alice = new SkytaleChannelManager({
|
|
38
|
+
identity: 'alice-agent',
|
|
39
|
+
apiKey: process.env.SKYTALE_API_KEY,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await alice.create('acme/support/routing');
|
|
43
|
+
const token = await alice.invite('acme/support/routing');
|
|
44
|
+
|
|
45
|
+
const bob = new SkytaleChannelManager({
|
|
46
|
+
identity: 'bob-agent',
|
|
47
|
+
apiKey: process.env.SKYTALE_API_KEY,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await bob.joinWithToken('acme/support/routing', token);
|
|
51
|
+
alice.send('acme/support/routing', 'Hello from Alice!');
|
|
52
|
+
const messages = await bob.receive('acme/support/routing');
|
|
53
|
+
console.log(messages); // ['Hello from Alice!']
|
|
54
|
+
|
|
55
|
+
alice.close();
|
|
56
|
+
bob.close();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### `SkytaleChannelManager`
|
|
62
|
+
|
|
63
|
+
High-level API for most use cases. Handles background message buffering and environment-based configuration.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { SkytaleChannelManager } from "@skytalesh/sdk";
|
|
67
|
+
|
|
68
|
+
const mgr = new SkytaleChannelManager({
|
|
69
|
+
identity: "my-agent", // string or Buffer
|
|
70
|
+
endpoint: "https://...", // defaults to SKYTALE_RELAY env
|
|
71
|
+
apiKey: "sk_live_...", // defaults to SKYTALE_API_KEY env
|
|
72
|
+
apiUrl: "https://...", // defaults to SKYTALE_API_URL env
|
|
73
|
+
mock: false, // enable mock mode for testing
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Constructor Parameters
|
|
78
|
+
|
|
79
|
+
| Parameter | Type | Required | Description |
|
|
80
|
+
|-----------|------|----------|-------------|
|
|
81
|
+
| `identity` | `string \| Buffer` | Yes | Agent identity (strings are UTF-8 encoded) |
|
|
82
|
+
| `endpoint` | `string` | No | Relay URL (default: `SKYTALE_RELAY` env or `https://relay.skytale.sh:5000`) |
|
|
83
|
+
| `dataDir` | `string` | No | MLS state directory (default: auto-generated) |
|
|
84
|
+
| `apiKey` | `string` | No | API key (default: `SKYTALE_API_KEY` env) |
|
|
85
|
+
| `apiUrl` | `string` | No | API server URL (default: `SKYTALE_API_URL` env or `https://api.skytale.sh`) |
|
|
86
|
+
| `mock` | `boolean` | No | Enable mock mode for testing without a relay (default: `false`) |
|
|
87
|
+
|
|
88
|
+
#### `create(channelName: string): Promise<void>`
|
|
89
|
+
|
|
90
|
+
Create a channel and start a background listener.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await mgr.create("myorg/team/general");
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### `invite(channelName: string, maxUses?: number, ttl?: number): Promise<string>`
|
|
97
|
+
|
|
98
|
+
Generate an invite token for a channel. Returns an `skt_inv_...` token.
|
|
99
|
+
|
|
100
|
+
- `maxUses` — maximum number of times the token can be used (default: 1)
|
|
101
|
+
- `ttl` — token lifetime in seconds (default: 3600)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const token = await mgr.invite("myorg/team/general");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### `joinWithToken(channelName: string, token: string, timeout?: number): Promise<void>`
|
|
108
|
+
|
|
109
|
+
Join a channel using an invite token. MLS key exchange is handled automatically.
|
|
110
|
+
|
|
111
|
+
- `timeout` — max milliseconds to wait (default: 60000)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
await mgr.joinWithToken("myorg/team/general", token);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `send(channelName: string, message: string | Buffer): void`
|
|
118
|
+
|
|
119
|
+
Send a message. Strings are UTF-8 encoded automatically.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
mgr.send("myorg/team/general", "Hello!");
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `receive(channelName: string, timeout?: number): Promise<string[]>`
|
|
126
|
+
|
|
127
|
+
Drain all buffered messages. Waits up to `timeout` ms (default: 5000) if buffer is empty.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const msgs = await mgr.receive("myorg/team/general");
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `receiveLatest(channelName: string, timeout?: number): Promise<string | null>`
|
|
134
|
+
|
|
135
|
+
Return only the most recent message, discarding older ones.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const latest = await mgr.receiveLatest("myorg/team/general");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `sendEnvelope(channelName: string, envelope: Envelope): void`
|
|
142
|
+
|
|
143
|
+
Send a structured envelope on a channel.
|
|
144
|
+
|
|
145
|
+
#### `receiveEnvelopes(channelName: string, timeout?: number): Promise<Envelope[]>`
|
|
146
|
+
|
|
147
|
+
Receive structured envelopes. Raw messages are auto-wrapped as `Protocol.RAW`.
|
|
148
|
+
|
|
149
|
+
#### `listChannels(): string[]`
|
|
150
|
+
|
|
151
|
+
Return names of all active channels.
|
|
152
|
+
|
|
153
|
+
#### `close(): void`
|
|
154
|
+
|
|
155
|
+
Stop all background listeners and release resources.
|
|
156
|
+
|
|
157
|
+
#### `on('message', (channelName: string, message: Buffer) => void)`
|
|
158
|
+
|
|
159
|
+
Event emitter for real-time message handling. Fires for every incoming message on any channel.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
mgr.on("message", (channel, msg) => {
|
|
163
|
+
console.log(`[${channel}] ${msg.toString()}`);
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## MCP Server
|
|
168
|
+
|
|
169
|
+
Expose Skytale encrypted channels as MCP tools for Claude Desktop, Cursor, or any MCP client.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npx @skytalesh/mcp-server
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Claude Desktop Configuration
|
|
176
|
+
|
|
177
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"mcpServers": {
|
|
182
|
+
"skytale": {
|
|
183
|
+
"command": "npx",
|
|
184
|
+
"args": ["@skytalesh/mcp-server"],
|
|
185
|
+
"env": {
|
|
186
|
+
"SKYTALE_API_KEY": "sk_live_..."
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Cursor Configuration
|
|
194
|
+
|
|
195
|
+
Add to `.cursor/mcp.json`:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"mcpServers": {
|
|
200
|
+
"skytale": {
|
|
201
|
+
"command": "npx",
|
|
202
|
+
"args": ["@skytalesh/mcp-server"]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### MCP Tools
|
|
209
|
+
|
|
210
|
+
| Tool | Description |
|
|
211
|
+
|------|-------------|
|
|
212
|
+
| `skytale_create` | Create an encrypted channel |
|
|
213
|
+
| `skytale_invite` | Generate an invite token for a channel |
|
|
214
|
+
| `skytale_join` | Join a channel with an invite token |
|
|
215
|
+
| `skytale_send` | Send a message on a channel |
|
|
216
|
+
| `skytale_receive` | Receive messages from a channel |
|
|
217
|
+
| `skytale_channels` | List active channels |
|
|
218
|
+
|
|
219
|
+
## MCP Encrypted Transport
|
|
220
|
+
|
|
221
|
+
Use Skytale as the transport layer for MCP protocol messages, replacing plaintext HTTP/stdio with MLS-encrypted channels.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { SkytaleChannelManager } from "@skytalesh/sdk";
|
|
225
|
+
import { SkytaleTransport } from "@skytalesh/sdk/mcp-transport";
|
|
226
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
227
|
+
|
|
228
|
+
const mgr = new SkytaleChannelManager({ identity: "agent-1", mock: true });
|
|
229
|
+
await mgr.create("org/ns/mcp-rpc");
|
|
230
|
+
|
|
231
|
+
const transport = new SkytaleTransport(mgr, "org/ns/mcp-rpc");
|
|
232
|
+
const client = new Client({ name: "my-client", version: "1.0.0" });
|
|
233
|
+
await client.connect(transport);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Mock Mode
|
|
237
|
+
|
|
238
|
+
Test agent logic without a running relay:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const mgr = new SkytaleChannelManager({ identity: "test-agent", mock: true });
|
|
242
|
+
await mgr.create("org/ns/test");
|
|
243
|
+
mgr.send("org/ns/test", "hello");
|
|
244
|
+
const msgs = await mgr.receive("org/ns/test"); // ["hello"]
|
|
245
|
+
mgr.close();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Mock mode is useful for unit tests, CI pipelines, and local development. Also enabled by `SKYTALE_MOCK=1` env var.
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
All methods throw typed exceptions extending `SkytaleError`:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { SkytaleChannelManager, ChannelError, AuthError, SkytaleError } from "@skytalesh/sdk";
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const mgr = new SkytaleChannelManager({ identity: "agent" });
|
|
259
|
+
await mgr.create("bad-name");
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e instanceof AuthError) {
|
|
262
|
+
console.log(`Auth failed (HTTP ${e.httpStatus}): ${e.message}`);
|
|
263
|
+
console.log(`Fix: ${e.docUrl}`);
|
|
264
|
+
} else if (e instanceof ChannelError) {
|
|
265
|
+
console.log(`Channel error [${e.code}]: ${e.message}`);
|
|
266
|
+
} else if (e instanceof SkytaleError) {
|
|
267
|
+
console.log(`SDK error: ${e.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Error Types
|
|
273
|
+
|
|
274
|
+
| Exception | When |
|
|
275
|
+
|-----------|------|
|
|
276
|
+
| `AuthError` | API key invalid, expired, or missing |
|
|
277
|
+
| `TransportError` | Relay unreachable, connection error, timeout |
|
|
278
|
+
| `ChannelError` | Invalid channel name, channel not found |
|
|
279
|
+
| `MlsError` | Bad key package, invalid Welcome, decryption failure |
|
|
280
|
+
| `QuotaExceededError` | Free tier message limit reached |
|
|
281
|
+
|
|
282
|
+
Every exception includes `code`, `httpStatus`, and `docUrl` attributes.
|
|
283
|
+
|
|
284
|
+
## Environment Variables
|
|
285
|
+
|
|
286
|
+
| Variable | Description | Default |
|
|
287
|
+
|----------|-------------|---------|
|
|
288
|
+
| `SKYTALE_RELAY` | Relay server URL | `https://relay.skytale.sh:5000` |
|
|
289
|
+
| `SKYTALE_API_KEY` | API key for authentication | -- |
|
|
290
|
+
| `SKYTALE_API_URL` | API server URL | `https://api.skytale.sh` |
|
|
291
|
+
| `SKYTALE_DATA_DIR` | MLS state directory | Auto-generated |
|
|
292
|
+
| `SKYTALE_IDENTITY` | Default agent identity | -- |
|
|
293
|
+
| `SKYTALE_MOCK` | Enable mock mode (`1`, `true`, `yes`) | `false` |
|
|
294
|
+
|
|
295
|
+
## Envelope & Protocol
|
|
296
|
+
|
|
297
|
+
Protocol-tagged messages for multi-protocol channels:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { Protocol, type Envelope, serializeEnvelope, deserializeEnvelope } from "@skytalesh/sdk";
|
|
301
|
+
|
|
302
|
+
const env: Envelope = {
|
|
303
|
+
protocol: Protocol.A2A,
|
|
304
|
+
contentType: "application/json",
|
|
305
|
+
payload: Buffer.from('{"parts":[]}'),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const data = serializeEnvelope(env);
|
|
309
|
+
const env2 = deserializeEnvelope(data);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Supported Protocols
|
|
313
|
+
|
|
314
|
+
| Value | Description |
|
|
315
|
+
|-------|-------------|
|
|
316
|
+
| `Protocol.RAW` | Plain bytes (default, backward compatible) |
|
|
317
|
+
| `Protocol.A2A` | Google Agent-to-Agent protocol |
|
|
318
|
+
| `Protocol.MCP` | Model Context Protocol |
|
|
319
|
+
| `Protocol.SLIM` | SLIM (Secure Lightweight Inter-agent Messaging) |
|
|
320
|
+
| `Protocol.ACP` | IBM Agent Communication Protocol |
|
|
321
|
+
| `Protocol.ANP` | Agent Network Protocol (DID-based) |
|
|
322
|
+
| `Protocol.LMOS` | Eclipse LMOS (Web of Things) |
|
|
323
|
+
| `Protocol.NLIP` | Natural Language Interaction Protocol |
|
|
324
|
+
|
|
325
|
+
## Architecture
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
Your Agent --> SkytaleChannelManager --> gRPC --> Relay --> gRPC --> SkytaleChannelManager --> Their Agent
|
|
329
|
+
| |
|
|
330
|
+
+-- MLS encrypt MLS decrypt --+
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
All messages are encrypted end-to-end with MLS (RFC 9420). The relay cannot read message contents.
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
Apache 2.0
|
package/js/api.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight HTTP client for the Skytale API server.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in `fetch` (Node 18+) to avoid external dependencies.
|
|
5
|
+
* All methods authenticate with the API key directly — the API server
|
|
6
|
+
* accepts both `sk_live_*` keys and JWTs in the Authorization header.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { SkytaleAPI } from "@skytalesh/sdk";
|
|
11
|
+
*
|
|
12
|
+
* const api = new SkytaleAPI("https://api.skytale.sh", "sk_live_...");
|
|
13
|
+
* await api.registerChannel("org/ns/svc");
|
|
14
|
+
* const resp = await api.createInvite("org/ns/svc");
|
|
15
|
+
* console.log(resp.token); // "skt_inv_..."
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class SkytaleAPI {
|
|
19
|
+
private readonly base;
|
|
20
|
+
private readonly apiKey;
|
|
21
|
+
private readonly maxRetries;
|
|
22
|
+
constructor(apiUrl: string, apiKey: string, maxRetries?: number);
|
|
23
|
+
private request;
|
|
24
|
+
registerChannel(name: string): Promise<{
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}>;
|
|
28
|
+
createInvite(channel: string, maxUses?: number, ttlSeconds?: number): Promise<{
|
|
29
|
+
token: string;
|
|
30
|
+
expires_at: string;
|
|
31
|
+
}>;
|
|
32
|
+
join(channel: string, token: string, keyPackageB64: string, identity: string): Promise<{
|
|
33
|
+
request_id: string;
|
|
34
|
+
}>;
|
|
35
|
+
getPending(channel: string): Promise<{
|
|
36
|
+
requests: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
identity: string;
|
|
39
|
+
key_package: string;
|
|
40
|
+
created_at: string;
|
|
41
|
+
}>;
|
|
42
|
+
}>;
|
|
43
|
+
submitWelcome(requestId: string, welcomeB64: string): Promise<{
|
|
44
|
+
status: string;
|
|
45
|
+
}>;
|
|
46
|
+
getWelcome(requestId: string): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
welcome?: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
package/js/api.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight HTTP client for the Skytale API server.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node.js built-in `fetch` (Node 18+) to avoid external dependencies.
|
|
6
|
+
* All methods authenticate with the API key directly — the API server
|
|
7
|
+
* accepts both `sk_live_*` keys and JWTs in the Authorization header.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { SkytaleAPI } from "@skytalesh/sdk";
|
|
12
|
+
*
|
|
13
|
+
* const api = new SkytaleAPI("https://api.skytale.sh", "sk_live_...");
|
|
14
|
+
* await api.registerChannel("org/ns/svc");
|
|
15
|
+
* const resp = await api.createInvite("org/ns/svc");
|
|
16
|
+
* console.log(resp.token); // "skt_inv_..."
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.SkytaleAPI = void 0;
|
|
21
|
+
const errors_1 = require("./errors");
|
|
22
|
+
const RETRYABLE_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
23
|
+
function validateApiUrl(apiUrl) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(apiUrl);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`Invalid API URL: ${apiUrl}`);
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol === "https:")
|
|
32
|
+
return;
|
|
33
|
+
if (parsed.protocol === "http:" &&
|
|
34
|
+
(parsed.hostname === "localhost" ||
|
|
35
|
+
parsed.hostname === "127.0.0.1" ||
|
|
36
|
+
parsed.hostname === "::1")) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`API URL must use HTTPS (got ${parsed.protocol}//${parsed.hostname}). ` +
|
|
40
|
+
"HTTP is only allowed for localhost/127.0.0.1.");
|
|
41
|
+
}
|
|
42
|
+
function backoffDelay(attempt, retryAfter) {
|
|
43
|
+
if (retryAfter != null) {
|
|
44
|
+
const ra = parseFloat(retryAfter);
|
|
45
|
+
if (!isNaN(ra) && ra > 0 && ra <= 60)
|
|
46
|
+
return ra * 1000;
|
|
47
|
+
}
|
|
48
|
+
const base = Math.min(0.5 * 2 ** attempt, 5.0);
|
|
49
|
+
const jitter = 0.75 + 0.25 * Math.random();
|
|
50
|
+
return base * jitter * 1000;
|
|
51
|
+
}
|
|
52
|
+
function mapApiError(httpCode, errorCode, message) {
|
|
53
|
+
if (errorCode === "unauthorized" || httpCode === 401) {
|
|
54
|
+
return new errors_1.AuthError("unable to authenticate: API key is invalid or expired. " +
|
|
55
|
+
"Verify SKYTALE_API_KEY is set correctly, or generate a new key " +
|
|
56
|
+
"with `skytale keys create`.", { code: "unauthorized", httpStatus: httpCode });
|
|
57
|
+
}
|
|
58
|
+
if (errorCode === "quota_exceeded" || httpCode === 429) {
|
|
59
|
+
return new errors_1.QuotaExceededError("unable to send: monthly message quota exceeded. " +
|
|
60
|
+
"Upgrade your plan at https://skytale.sh/billing or wait for " +
|
|
61
|
+
"the next billing cycle.", { code: "quota_exceeded", httpStatus: httpCode });
|
|
62
|
+
}
|
|
63
|
+
if (errorCode === "not_found" || httpCode === 404) {
|
|
64
|
+
return new errors_1.ChannelError(`unable to find resource: ${message}. ` +
|
|
65
|
+
"Verify the channel name uses org/namespace/service format.", { code: "not_found", httpStatus: httpCode });
|
|
66
|
+
}
|
|
67
|
+
if (errorCode === "bad_request" || errorCode === "conflict") {
|
|
68
|
+
return new errors_1.ChannelError(`invalid request: ${message}`, {
|
|
69
|
+
code: errorCode,
|
|
70
|
+
httpStatus: httpCode,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (httpCode >= 500) {
|
|
74
|
+
return new errors_1.TransportError(`API server error (${httpCode}): ${message}. ` +
|
|
75
|
+
"Check https://status.skytale.sh for service status.", { code: "server_error", httpStatus: httpCode });
|
|
76
|
+
}
|
|
77
|
+
return new errors_1.SkytaleError(`API error (${httpCode}): ${message}`, {
|
|
78
|
+
code: errorCode || "api_error",
|
|
79
|
+
httpStatus: httpCode,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
class SkytaleAPI {
|
|
83
|
+
base;
|
|
84
|
+
apiKey;
|
|
85
|
+
maxRetries;
|
|
86
|
+
constructor(apiUrl, apiKey, maxRetries = 2) {
|
|
87
|
+
validateApiUrl(apiUrl);
|
|
88
|
+
this.base = apiUrl.replace(/\/+$/, "");
|
|
89
|
+
this.apiKey = apiKey;
|
|
90
|
+
this.maxRetries = maxRetries;
|
|
91
|
+
}
|
|
92
|
+
async request(method, path, body) {
|
|
93
|
+
const url = `${this.base}${path}`;
|
|
94
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await fetch(url, {
|
|
98
|
+
method,
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
},
|
|
104
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
105
|
+
signal: AbortSignal.timeout(30_000),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (attempt < this.maxRetries) {
|
|
110
|
+
const delay = backoffDelay(attempt);
|
|
111
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw new errors_1.TransportError(`unable to reach API at ${url}: ${e}. ` +
|
|
115
|
+
"Check your network connection and https://status.skytale.sh", { code: "connection_error" });
|
|
116
|
+
}
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
return (await response.json());
|
|
119
|
+
}
|
|
120
|
+
if (RETRYABLE_CODES.has(response.status) && attempt < this.maxRetries) {
|
|
121
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
122
|
+
const delay = backoffDelay(attempt, retryAfter);
|
|
123
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let msg;
|
|
127
|
+
let code = "";
|
|
128
|
+
try {
|
|
129
|
+
const errorBody = (await response.json());
|
|
130
|
+
msg = errorBody.error ?? response.statusText;
|
|
131
|
+
code = errorBody.code ?? "";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
msg = response.statusText;
|
|
135
|
+
}
|
|
136
|
+
throw mapApiError(response.status, code, msg);
|
|
137
|
+
}
|
|
138
|
+
throw new errors_1.TransportError(`request to ${url} failed after ${this.maxRetries} retries`, { code: "max_retries_exceeded" });
|
|
139
|
+
}
|
|
140
|
+
// ----------------------------------------------------------------
|
|
141
|
+
// Channel registration
|
|
142
|
+
// ----------------------------------------------------------------
|
|
143
|
+
async registerChannel(name) {
|
|
144
|
+
return (await this.request("POST", "/v1/channels", { name }));
|
|
145
|
+
}
|
|
146
|
+
// ----------------------------------------------------------------
|
|
147
|
+
// Invite tokens
|
|
148
|
+
// ----------------------------------------------------------------
|
|
149
|
+
async createInvite(channel, maxUses = 1, ttlSeconds = 3600) {
|
|
150
|
+
return (await this.request("POST", "/v1/channels/invites", {
|
|
151
|
+
channel,
|
|
152
|
+
max_uses: maxUses,
|
|
153
|
+
ttl_seconds: ttlSeconds,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
// ----------------------------------------------------------------
|
|
157
|
+
// Join flow
|
|
158
|
+
// ----------------------------------------------------------------
|
|
159
|
+
async join(channel, token, keyPackageB64, identity) {
|
|
160
|
+
return (await this.request("POST", "/v1/channels/join", {
|
|
161
|
+
channel,
|
|
162
|
+
token,
|
|
163
|
+
key_package: keyPackageB64,
|
|
164
|
+
identity,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
async getPending(channel) {
|
|
168
|
+
const encoded = encodeURIComponent(channel);
|
|
169
|
+
return (await this.request("GET", `/v1/channels/pending?channel=${encoded}`));
|
|
170
|
+
}
|
|
171
|
+
async submitWelcome(requestId, welcomeB64) {
|
|
172
|
+
return (await this.request("POST", "/v1/channels/welcome", {
|
|
173
|
+
request_id: requestId,
|
|
174
|
+
welcome: welcomeB64,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
async getWelcome(requestId) {
|
|
178
|
+
return (await this.request("GET", `/v1/channels/welcome/${requestId}`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.SkytaleAPI = SkytaleAPI;
|