@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 ADDED
@@ -0,0 +1,337 @@
1
+ # @skytalesh/sdk
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@skytalesh/sdk.svg)](https://www.npmjs.com/package/@skytalesh/sdk)
4
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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;