@layr8/sdk 0.1.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 +582 -0
- package/dist/backoff.d.ts +10 -0
- package/dist/backoff.d.ts.map +1 -0
- package/dist/backoff.js +20 -0
- package/dist/backoff.js.map +1 -0
- package/dist/channel.d.ts +88 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +522 -0
- package/dist/channel.js.map +1 -0
- package/dist/client.d.ts +119 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +509 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +55 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +5 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +132 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +28 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +51 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/message.d.ts +67 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +77 -0
- package/dist/message.js.map +1 -0
- package/dist/presentations.d.ts +24 -0
- package/dist/presentations.d.ts.map +1 -0
- package/dist/presentations.js +5 -0
- package/dist/presentations.js.map +1 -0
- package/dist/rest.d.ts +33 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +150 -0
- package/dist/rest.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# Layr8 Node.js SDK
|
|
2
|
+
|
|
3
|
+
The official Node.js SDK for building agents on the [Layr8](https://layr8.com) platform. Agents connect to Layr8 cloud-nodes via WebSocket and exchange [DIDComm v2](https://identity.foundation/didcomm-messaging/spec/) messages with other agents across the network.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @layr8/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 20 or later. The package is ESM-only (`"type": "module"`).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Layr8Client, unmarshalBody, logErrors } from "@layr8/sdk";
|
|
17
|
+
import type { Message } from "@layr8/sdk";
|
|
18
|
+
|
|
19
|
+
interface EchoRequest {
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const client = new Layr8Client(logErrors(), {
|
|
24
|
+
nodeUrl: "ws://localhost:4000/plugin_socket/websocket",
|
|
25
|
+
apiKey: "your-api-key",
|
|
26
|
+
agentDid: "did:web:myorg:my-agent",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
client.handle(
|
|
30
|
+
"https://layr8.io/protocols/echo/1.0/request",
|
|
31
|
+
async (msg: Message): Promise<Message | null> => {
|
|
32
|
+
const body = unmarshalBody<EchoRequest>(msg as any);
|
|
33
|
+
return {
|
|
34
|
+
id: "",
|
|
35
|
+
type: "https://layr8.io/protocols/echo/1.0/response",
|
|
36
|
+
from: "",
|
|
37
|
+
to: [],
|
|
38
|
+
threadId: "",
|
|
39
|
+
parentThreadId: "",
|
|
40
|
+
body: { echo: body.message },
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await client.connect();
|
|
46
|
+
console.log(`agent running as ${client.did}`);
|
|
47
|
+
|
|
48
|
+
process.on("SIGINT", async () => {
|
|
49
|
+
await client.close();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Core Concepts
|
|
55
|
+
|
|
56
|
+
### Client
|
|
57
|
+
|
|
58
|
+
The `Layr8Client` is the main entry point. It manages the WebSocket connection to a cloud-node, routes inbound messages to handlers, and provides methods for sending outbound messages.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const client = new Layr8Client(logErrors(), {...});
|
|
62
|
+
|
|
63
|
+
// Register handlers before connecting
|
|
64
|
+
client.handle(messageType, handlerFn);
|
|
65
|
+
|
|
66
|
+
// Connect to the cloud-node
|
|
67
|
+
await client.connect();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Messages
|
|
71
|
+
|
|
72
|
+
`Message` represents a DIDComm v2 message with standard fields:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
interface Message {
|
|
76
|
+
id: string; // unique message ID (auto-generated if empty)
|
|
77
|
+
type: string; // DIDComm message type URI
|
|
78
|
+
from: string; // sender DID (auto-filled from client)
|
|
79
|
+
to: string[]; // recipient DIDs
|
|
80
|
+
threadId: string; // thread correlation ID
|
|
81
|
+
parentThreadId: string; // parent thread for nested conversations
|
|
82
|
+
body: unknown; // message payload (serialized to JSON)
|
|
83
|
+
context?: MessageContext; // cloud-node metadata (inbound only)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Decode the body of an inbound message with `unmarshalBody`:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const req = unmarshalBody<MyRequest>(msg as any);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Handlers
|
|
94
|
+
|
|
95
|
+
Handlers process inbound messages. Register them with `client.handle()` before calling `connect()`.
|
|
96
|
+
|
|
97
|
+
A handler receives a `Message` and returns:
|
|
98
|
+
|
|
99
|
+
| Return value | Behavior |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `Message` | Sends response to the sender. `from`, `to`, and `threadId` are auto-filled. |
|
|
102
|
+
| `null` | Fire-and-forget — no response sent. |
|
|
103
|
+
| Thrown error | Sends a DIDComm [problem report](https://identity.foundation/didcomm-messaging/spec/#problem-reports) to the sender. |
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
client.handle(
|
|
107
|
+
"https://layr8.io/protocols/echo/1.0/request",
|
|
108
|
+
async (msg: Message): Promise<Message | null> => {
|
|
109
|
+
const body = unmarshalBody<EchoRequest>(msg as any);
|
|
110
|
+
return {
|
|
111
|
+
id: "",
|
|
112
|
+
type: "https://layr8.io/protocols/echo/1.0/response",
|
|
113
|
+
from: "",
|
|
114
|
+
to: [],
|
|
115
|
+
threadId: "",
|
|
116
|
+
parentThreadId: "",
|
|
117
|
+
body: { echo: body.message },
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Protocol Registration
|
|
124
|
+
|
|
125
|
+
The SDK automatically derives protocol base URIs from your handler message types and registers them with the cloud-node on connect. For example, handling `https://layr8.io/protocols/echo/1.0/request` registers the protocol `https://layr8.io/protocols/echo/1.0`.
|
|
126
|
+
|
|
127
|
+
## Sending Messages
|
|
128
|
+
|
|
129
|
+
### Send
|
|
130
|
+
|
|
131
|
+
Send a one-way message. By default, `send()` waits for the server to acknowledge receipt:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
await client.send({
|
|
135
|
+
type: "https://didcomm.org/basicmessage/2.0/message",
|
|
136
|
+
to: ["did:web:other-org:their-agent"],
|
|
137
|
+
body: { content: "hello!" },
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`send()` accepts `Partial<Message>` — only `type`, `to`, and `body` are required.
|
|
142
|
+
|
|
143
|
+
To skip waiting for the server acknowledgment, pass `{ fireAndForget: true }`:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
await client.send(
|
|
147
|
+
{
|
|
148
|
+
type: "https://didcomm.org/basicmessage/2.0/message",
|
|
149
|
+
to: ["did:web:other-org:their-agent"],
|
|
150
|
+
body: { content: "hello!" },
|
|
151
|
+
},
|
|
152
|
+
{ fireAndForget: true },
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Send Options
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
interface SendOptions {
|
|
160
|
+
fireAndForget?: boolean; // skip waiting for server ack (default: false)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Request (Request/Response)
|
|
165
|
+
|
|
166
|
+
Send a message and await a correlated response:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const resp = await client.request(
|
|
170
|
+
{
|
|
171
|
+
type: "https://layr8.io/protocols/echo/1.0/request",
|
|
172
|
+
to: ["did:web:other-org:echo-agent"],
|
|
173
|
+
body: { message: "ping" },
|
|
174
|
+
},
|
|
175
|
+
{ signal: AbortSignal.timeout(5_000) },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = unmarshalBody<EchoResponse>(resp as any);
|
|
179
|
+
console.log(result.echo); // "ping"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Thread correlation is automatic — the SDK generates a `threadId`, attaches it to the outbound message, and matches the inbound response by the same `threadId`.
|
|
183
|
+
|
|
184
|
+
#### Request Options
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
interface RequestOptions {
|
|
188
|
+
parentThread?: string; // parent thread ID for nested conversations
|
|
189
|
+
signal?: AbortSignal; // abort/timeout control
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
Configuration can be set explicitly or via environment variables. Environment variables are used as fallbacks when the corresponding field is empty or undefined.
|
|
196
|
+
|
|
197
|
+
| Field | Environment Variable | Required | Description |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `nodeUrl` | `LAYR8_NODE_URL` | Yes | WebSocket URL of the cloud-node |
|
|
200
|
+
| `apiKey` | `LAYR8_API_KEY` | Yes | API key for authentication |
|
|
201
|
+
| `agentDid` | `LAYR8_AGENT_DID` | No | Agent DID identity |
|
|
202
|
+
|
|
203
|
+
If `agentDid` is not provided, the cloud-node creates an ephemeral DID on connect. Retrieve it with `client.did`.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Explicit configuration
|
|
207
|
+
const client = new Layr8Client(logErrors(), {
|
|
208
|
+
nodeUrl: "ws://localhost:4000/plugin_socket/websocket",
|
|
209
|
+
apiKey: "my-api-key",
|
|
210
|
+
agentDid: "did:web:myorg:my-agent",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Environment-only configuration
|
|
214
|
+
// Set LAYR8_NODE_URL, LAYR8_API_KEY, LAYR8_AGENT_DID
|
|
215
|
+
const client = new Layr8Client(logErrors());
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Handler Options
|
|
219
|
+
|
|
220
|
+
### Manual Acknowledgment
|
|
221
|
+
|
|
222
|
+
By default, messages are acknowledged to the cloud-node before the handler runs (auto-ack). For handlers where you need guaranteed processing, use manual ack to acknowledge only after successful execution. Unacknowledged messages are redelivered by the cloud-node.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { ack } from "@layr8/sdk";
|
|
226
|
+
|
|
227
|
+
client.handle(
|
|
228
|
+
queryType,
|
|
229
|
+
async (msg: Message): Promise<Message | null> => {
|
|
230
|
+
const result = await executeQuery(msg);
|
|
231
|
+
ack(msg as any); // explicitly acknowledge after success
|
|
232
|
+
return {
|
|
233
|
+
id: "", type: resultType, from: "", to: [],
|
|
234
|
+
threadId: "", parentThreadId: "",
|
|
235
|
+
body: result,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
{ manualAck: true },
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Connection Lifecycle
|
|
243
|
+
|
|
244
|
+
### DID Assignment
|
|
245
|
+
|
|
246
|
+
If no `agentDid` is configured, the cloud-node assigns an ephemeral DID on connect:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const client = new Layr8Client(logErrors(), {
|
|
250
|
+
nodeUrl: "ws://localhost:4000/plugin_socket/websocket",
|
|
251
|
+
apiKey: "my-key",
|
|
252
|
+
});
|
|
253
|
+
await client.connect();
|
|
254
|
+
|
|
255
|
+
console.log(client.did); // "did:web:myorg:abc123" (assigned by node)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Connection Resilience
|
|
259
|
+
|
|
260
|
+
The SDK automatically reconnects when the WebSocket connection drops (e.g., node restart, network interruption). Reconnection uses exponential backoff starting at 1 second, capped at 30 seconds.
|
|
261
|
+
|
|
262
|
+
During reconnection:
|
|
263
|
+
- `send()`, `request()`, and other operations throw `NotConnectedError` immediately — the SDK does not queue messages
|
|
264
|
+
- The `disconnect` event fires when the connection drops
|
|
265
|
+
- The `reconnect` event fires when the connection is restored
|
|
266
|
+
- `close()` stops the reconnect loop
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
client.on("disconnect", (err: Error) => {
|
|
270
|
+
console.log("disconnected:", err.message);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
client.on("reconnect", () => {
|
|
274
|
+
console.log("reconnected");
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Observability Hooks
|
|
279
|
+
|
|
280
|
+
For tools that need to surface raw DIDComm traffic (debugging, dashboards, MCP-style adapters that expose layr8 to other runtimes), the client emits events for every message it sends or receives. These fire alongside normal dispatch and don't change handler semantics.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
client.on("inbound", (msg: Message) => {
|
|
284
|
+
console.log("← recv", msg.type, "from", msg.from);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
client.on("outbound", (msg: Message) => {
|
|
288
|
+
console.log("→ send", msg.type, "to", msg.to);
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`inbound` fires after a message is successfully parsed, before it's routed to a handler or matched to a pending `request()`. `outbound` fires for every `send()`, `request()`, and handler auto-response.
|
|
293
|
+
|
|
294
|
+
### Default handler for unmatched types
|
|
295
|
+
|
|
296
|
+
When the cloud-node delivers a message whose type has no specific handler, the default behaviour is to fire `ErrorKind.NoHandler` via your error handler. To route those messages somewhere instead, register a default handler:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
client.handleDefault(async (msg: Message) => {
|
|
300
|
+
console.log("unmatched:", msg.type);
|
|
301
|
+
return null;
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The cloud-node only delivers messages whose **protocol** the client has subscribed to (derived from `handle()` registrations). The default handler catches types within a subscribed protocol that lack a specific handler — it does not cause the client to subscribe to additional protocols.
|
|
306
|
+
|
|
307
|
+
`handleDefault` runs with auto-ack only; `manualAck` is not supported on the fallback path. Use `handle(type, fn, { manualAck: true })` for types that need durable processing.
|
|
308
|
+
|
|
309
|
+
## Message Context
|
|
310
|
+
|
|
311
|
+
Inbound messages include a `context` field with metadata from the cloud-node:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
client.handle(messageType, async (msg: Message) => {
|
|
315
|
+
if (msg.context) {
|
|
316
|
+
console.log("Recipient:", msg.context.recipient);
|
|
317
|
+
console.log("Authorized:", msg.context.authorized);
|
|
318
|
+
|
|
319
|
+
for (const cred of msg.context.senderCredentials) {
|
|
320
|
+
console.log(`Sender credential: ${cred.name} (${cred.id})`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Field | Type | Description |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `recipient` | `string` | The DID that received this message |
|
|
330
|
+
| `authorized` | `boolean` | Whether the sender is authorized by the node's policy |
|
|
331
|
+
| `senderCredentials` | `Credential[]` | Verifiable credentials presented by the sender |
|
|
332
|
+
|
|
333
|
+
## Error Handling
|
|
334
|
+
|
|
335
|
+
### ErrorHandler (Required)
|
|
336
|
+
|
|
337
|
+
The `Layr8Client` constructor requires an `ErrorHandler` callback as its first argument. This ensures no SDK errors are silently dropped. The callback receives structured `SDKError` objects for parse failures, unhandled message types, handler exceptions, and server rejections.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { Layr8Client, logErrors } from "@layr8/sdk";
|
|
341
|
+
import type { ErrorHandler } from "@layr8/sdk";
|
|
342
|
+
|
|
343
|
+
// Use the built-in logger (writes to console.error)
|
|
344
|
+
const client = new Layr8Client(logErrors(), { ... });
|
|
345
|
+
|
|
346
|
+
// Or provide a custom handler
|
|
347
|
+
const onError: ErrorHandler = (err) => {
|
|
348
|
+
metrics.increment(`sdk.error.${err.kind}`);
|
|
349
|
+
logger.warn("SDK error", {
|
|
350
|
+
kind: err.kind,
|
|
351
|
+
messageId: err.messageId,
|
|
352
|
+
type: err.type,
|
|
353
|
+
cause: err.cause?.message,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
const client = new Layr8Client(onError, { ... });
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### SDKError
|
|
360
|
+
|
|
361
|
+
`SDKError` is a structured error report passed to the `ErrorHandler`. It carries machine-readable context about what went wrong:
|
|
362
|
+
|
|
363
|
+
| Field | Type | Description |
|
|
364
|
+
|---|---|---|
|
|
365
|
+
| `kind` | `ErrorKind` | Category of the error |
|
|
366
|
+
| `messageId` | `string` | ID of the message that caused the error (if available) |
|
|
367
|
+
| `type` | `string` | DIDComm message type (if available) |
|
|
368
|
+
| `from` | `string` | Sender DID (if available) |
|
|
369
|
+
| `cause` | `Error \| null` | Underlying error |
|
|
370
|
+
| `raw` | `unknown` | Raw payload for parse failures |
|
|
371
|
+
| `timestamp` | `Date` | When the error occurred |
|
|
372
|
+
|
|
373
|
+
### ErrorKind
|
|
374
|
+
|
|
375
|
+
| Kind | Description |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `ParseFailure` | Inbound message could not be parsed as DIDComm |
|
|
378
|
+
| `NoHandler` | No handler registered for the message type |
|
|
379
|
+
| `HandlerException` | A handler threw an exception |
|
|
380
|
+
| `ServerReject` | The server rejected a sent message |
|
|
381
|
+
| `TransportWrite` | Failed to write to the WebSocket connection |
|
|
382
|
+
|
|
383
|
+
### logErrors()
|
|
384
|
+
|
|
385
|
+
`logErrors()` returns a built-in `ErrorHandler` that logs every error to `console.error` with structured metadata. Use it as a sensible default:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { logErrors } from "@layr8/sdk";
|
|
389
|
+
|
|
390
|
+
const client = new Layr8Client(logErrors(), { ... });
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Problem Reports
|
|
394
|
+
|
|
395
|
+
When a handler throws an error, the SDK automatically sends a [DIDComm problem report](https://identity.foundation/didcomm-messaging/spec/#problem-reports) to the sender:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
client.handle(msgType, async (msg: Message) => {
|
|
399
|
+
throw new Error("something went wrong"); // sends problem report
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
When `request()` receives a problem report as the response, it throws a `ProblemReportError`:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { ProblemReportError } from "@layr8/sdk";
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const resp = await client.request(msg);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (err instanceof ProblemReportError) {
|
|
412
|
+
console.log(`Remote error [${err.code}]: ${err.comment}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Connection Errors
|
|
418
|
+
|
|
419
|
+
Connection failures throw a `ConnectionError`:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { ConnectionError } from "@layr8/sdk";
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await client.connect();
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (err instanceof ConnectionError) {
|
|
428
|
+
console.log(`Failed to connect to ${err.url}: ${err.reason}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Error Classes
|
|
434
|
+
|
|
435
|
+
| Error | Description |
|
|
436
|
+
|---|---|
|
|
437
|
+
| `NotConnectedError` | Operation attempted before `connect()` or after `close()` |
|
|
438
|
+
| `AlreadyConnectedError` | `handle()` called after `connect()` |
|
|
439
|
+
| `ClientClosedError` | `connect()` called on a closed client |
|
|
440
|
+
| `ProblemReportError` | Remote handler returned an error (`.code`, `.comment`) |
|
|
441
|
+
| `ConnectionError` | Failed to connect to cloud-node (`.url`, `.reason`) |
|
|
442
|
+
|
|
443
|
+
## W3C Verifiable Credentials
|
|
444
|
+
|
|
445
|
+
The SDK provides methods for signing, verifying, storing, listing, and retrieving [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/). These operations use the cloud-node's REST API and the DID keys in the node's wallet.
|
|
446
|
+
|
|
447
|
+
### Sign a Credential
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import type { Credential } from "@layr8/sdk";
|
|
451
|
+
|
|
452
|
+
const cred: Credential = {
|
|
453
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
454
|
+
id: "urn:uuid:my-credential",
|
|
455
|
+
type: ["VerifiableCredential"],
|
|
456
|
+
issuer: client.did,
|
|
457
|
+
credentialSubject: { id: "did:web:example:holder", name: "Alice" },
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const signedJWT = await client.signCredential(cred);
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Options: `{ issuerDid, format }`.
|
|
464
|
+
|
|
465
|
+
### Verify a Credential
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
const verified = await client.verifyCredential(signedJWT);
|
|
469
|
+
console.log(verified.credential); // decoded credential claims
|
|
470
|
+
console.log(verified.headers); // JWT headers (alg, kid, etc.)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Options: `{ verifierDid }`.
|
|
474
|
+
|
|
475
|
+
> **Note:** The verifier DID must have keys in the local node's wallet. Cross-node verification is not currently supported.
|
|
476
|
+
|
|
477
|
+
### Store, List, Get
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// Store a signed credential
|
|
481
|
+
const stored = await client.storeCredential(signedJWT);
|
|
482
|
+
console.log(stored.id); // storage ID
|
|
483
|
+
|
|
484
|
+
// List all stored credentials
|
|
485
|
+
const creds = await client.listCredentials();
|
|
486
|
+
|
|
487
|
+
// Retrieve by ID
|
|
488
|
+
const fetched = await client.getCredential(stored.id);
|
|
489
|
+
console.log(fetched.credential_jwt); // the original signed JWT
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Store options: `{ holderDid, issuerDid, validUntil }`.
|
|
493
|
+
List options: `{ holderDid }`.
|
|
494
|
+
|
|
495
|
+
### Output Formats
|
|
496
|
+
|
|
497
|
+
The `format` option accepts: `"compact_jwt"` (default), `"json"`, `"jwt"`, `"enveloped"`.
|
|
498
|
+
|
|
499
|
+
## W3C Verifiable Presentations
|
|
500
|
+
|
|
501
|
+
Presentations wrap one or more signed credentials into a holder-signed envelope.
|
|
502
|
+
|
|
503
|
+
### Sign a Presentation
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const signedPres = await client.signPresentation([signedJWT], {
|
|
507
|
+
nonce: "challenge-from-verifier",
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Options: `{ holderDid, format, nonce }`.
|
|
512
|
+
|
|
513
|
+
### Verify a Presentation
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
const verified = await client.verifyPresentation(signedPres);
|
|
517
|
+
console.log(verified.presentation); // decoded presentation claims
|
|
518
|
+
console.log(verified.headers); // JWT headers
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Options: `{ verifierDid }`.
|
|
522
|
+
|
|
523
|
+
## Examples
|
|
524
|
+
|
|
525
|
+
The [examples/](examples/) directory contains complete, runnable agents:
|
|
526
|
+
|
|
527
|
+
### Echo Agent
|
|
528
|
+
|
|
529
|
+
A minimal agent that echoes back any message it receives. Demonstrates request/response handlers with auto-ack, auto-thread correlation, and reconnection with backoff.
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
LAYR8_API_KEY=your-key npx tsx examples/echo-agent.ts
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Chat Client
|
|
536
|
+
|
|
537
|
+
An interactive chat client for DIDComm basic messaging. Demonstrates fire-and-forget `send()`, inbound message handling, `MessageContext` for sender credentials, and multi-recipient messaging.
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
LAYR8_API_KEY=your-key npx tsx examples/chat.ts did:web:friend:chat-agent
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Durable Handler
|
|
544
|
+
|
|
545
|
+
Persist-then-ack pattern: writes inbound messages to a JSON-lines file before acknowledging. If the process crashes before ack, the cloud-node redelivers. Demonstrates `manualAck` with zero external dependencies.
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
LAYR8_API_KEY=your-key npx tsx examples/durable-handler.ts
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Development
|
|
552
|
+
|
|
553
|
+
### Prerequisites
|
|
554
|
+
|
|
555
|
+
- Node.js 20+
|
|
556
|
+
- npm
|
|
557
|
+
|
|
558
|
+
### Scripts
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
npm test # Run unit tests (vitest)
|
|
562
|
+
npm run test:watch # Run tests in watch mode
|
|
563
|
+
npm run build # Compile TypeScript
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Architecture
|
|
567
|
+
|
|
568
|
+
The SDK is structured around a small set of types:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
Layr8Client → public API (connect, send, request, handle, close)
|
|
572
|
+
├── Config → configuration with env var fallback
|
|
573
|
+
├── Message → DIDComm v2 message envelope
|
|
574
|
+
├── Handler → message type → handler function registry
|
|
575
|
+
└── Channel → WebSocket/Phoenix Channel transport
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The transport layer implements the Phoenix Channel V2 wire protocol over WebSocket, including join negotiation, heartbeats, and message acknowledgment.
|
|
579
|
+
|
|
580
|
+
## License
|
|
581
|
+
|
|
582
|
+
Copyright Layr8 Inc. All rights reserved.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Exponential backoff with a maximum delay. */
|
|
2
|
+
export declare class Backoff {
|
|
3
|
+
private readonly initial;
|
|
4
|
+
private readonly max;
|
|
5
|
+
private current;
|
|
6
|
+
constructor(initial: number, max: number);
|
|
7
|
+
next(): number;
|
|
8
|
+
reset(): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,qBAAa,OAAO;IAIhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJtB,OAAO,CAAC,OAAO,CAAS;gBAGL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM;IAK9B,IAAI,IAAI,MAAM;IAMd,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/backoff.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Exponential backoff with a maximum delay. */
|
|
2
|
+
export class Backoff {
|
|
3
|
+
initial;
|
|
4
|
+
max;
|
|
5
|
+
current;
|
|
6
|
+
constructor(initial, max) {
|
|
7
|
+
this.initial = initial;
|
|
8
|
+
this.max = max;
|
|
9
|
+
this.current = initial;
|
|
10
|
+
}
|
|
11
|
+
next() {
|
|
12
|
+
const d = Math.min(this.current, this.max);
|
|
13
|
+
this.current = Math.min(this.current * 2, this.max);
|
|
14
|
+
return d;
|
|
15
|
+
}
|
|
16
|
+
reset() {
|
|
17
|
+
this.current = this.initial;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,MAAM,OAAO,OAAO;IAIC;IACA;IAJX,OAAO,CAAS;IAExB,YACmB,OAAe,EACf,GAAW;QADX,YAAO,GAAP,OAAO,CAAQ;QACf,QAAG,GAAH,GAAG,CAAQ;QAE5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,IAAI;QACF,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;CACF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { DidSpec } from "./config.js";
|
|
2
|
+
/** Server reply received for a tracked message ref. */
|
|
3
|
+
export interface ServerReply {
|
|
4
|
+
status: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ChannelCallbacks {
|
|
8
|
+
onMessage: (payload: unknown) => void;
|
|
9
|
+
onDisconnect?: (err: Error) => void;
|
|
10
|
+
onReconnect?: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Phoenix Channel transport over WebSocket.
|
|
14
|
+
* Implements the same protocol as the Go SDK's phoenixChannel.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PhoenixChannel {
|
|
17
|
+
private readonly wsUrl;
|
|
18
|
+
private readonly apiKey;
|
|
19
|
+
private ws;
|
|
20
|
+
private refCounter;
|
|
21
|
+
private joinRef;
|
|
22
|
+
private readonly topic;
|
|
23
|
+
private callbacks;
|
|
24
|
+
private pendingJoinResolve;
|
|
25
|
+
private closed;
|
|
26
|
+
private reconnecting;
|
|
27
|
+
private protocols;
|
|
28
|
+
private heartbeatTimer;
|
|
29
|
+
/**
|
|
30
|
+
* Monotonic timestamp (Date.now()) of the most recently observed inbound
|
|
31
|
+
* frame — any message, pong, or phx_reply. Used by the Phoenix-level
|
|
32
|
+
* watchdog in startHeartbeat to detect "TCP healthy but Phoenix Channel
|
|
33
|
+
* GenServer hung" failures: when the heartbeat tick observes no frames
|
|
34
|
+
* for HEARTBEAT_MAX_SILENT_MS, it forces a close that the existing
|
|
35
|
+
* reconnect path picks up.
|
|
36
|
+
*/
|
|
37
|
+
private lastFrameAt;
|
|
38
|
+
/**
|
|
39
|
+
* Timer that periodically emits a WS-level `{:ping, _}` frame. Independent
|
|
40
|
+
* of the Phoenix heartbeat — covers the TCP / NAT / LB half-dead case
|
|
41
|
+
* where the connection is unilaterally killed without a FIN.
|
|
42
|
+
*/
|
|
43
|
+
private wsPingTimer;
|
|
44
|
+
/**
|
|
45
|
+
* Timer armed each time we emit a WS ping. Cleared on any inbound frame
|
|
46
|
+
* (pong, message, phx_reply — anything proves liveness). If it fires we
|
|
47
|
+
* force-close the WS so the existing reconnect path takes over.
|
|
48
|
+
*/
|
|
49
|
+
private pongWaitTimer;
|
|
50
|
+
private assignedDIDVal;
|
|
51
|
+
private readonly pendingRefs;
|
|
52
|
+
private static readonly HEARTBEAT_INTERVAL_MS;
|
|
53
|
+
private static readonly HEARTBEAT_MAX_SILENT_MS;
|
|
54
|
+
private static readonly WS_PING_INTERVAL_MS;
|
|
55
|
+
private static readonly WS_PONG_WAIT_MS;
|
|
56
|
+
private replyProtocolEnabled;
|
|
57
|
+
private readonly didSpec;
|
|
58
|
+
constructor(wsUrl: string, apiKey: string, agentDid: string, callbacks: ChannelCallbacks, didSpec?: DidSpec);
|
|
59
|
+
connect(protocols: string[], signal?: AbortSignal): Promise<void>;
|
|
60
|
+
private dial;
|
|
61
|
+
private join;
|
|
62
|
+
send(event: string, payload: unknown): Promise<ServerReply>;
|
|
63
|
+
sendFireAndForget(event: string, payload: unknown): void;
|
|
64
|
+
sendAck(ids: string[]): void;
|
|
65
|
+
assignedDID(): string;
|
|
66
|
+
/** Whether the server supports the reply protocol (capability negotiated at join). */
|
|
67
|
+
replyProtocol(): boolean;
|
|
68
|
+
close(): void;
|
|
69
|
+
private setupReadLoop;
|
|
70
|
+
private handleInbound;
|
|
71
|
+
private startHeartbeat;
|
|
72
|
+
/**
|
|
73
|
+
* WS-level ping every WS_PING_INTERVAL_MS. Cowboy (Phoenix's HTTP/WS
|
|
74
|
+
* server) auto-pongs at the protocol layer — even if the application
|
|
75
|
+
* Channel is hung. When the underlying TCP / NAT / LB has silently
|
|
76
|
+
* dropped the connection, ping write may succeed locally but no pong
|
|
77
|
+
* comes back; pongWaitTimer fires and we force-close.
|
|
78
|
+
*/
|
|
79
|
+
private startWsPingLoop;
|
|
80
|
+
private armPongWait;
|
|
81
|
+
private disarmPongWait;
|
|
82
|
+
private stopLivenessTimers;
|
|
83
|
+
private reconnectLoop;
|
|
84
|
+
private rejectPendingRefs;
|
|
85
|
+
private nextRef;
|
|
86
|
+
private writeMsg;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=channel.d.ts.map
|