vessels-sdk 0.2.0 → 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 +414 -46
- package/dist/index.cjs +56 -14
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +56 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vessels-sdk
|
|
2
2
|
|
|
3
|
-
Node.js SDK for [Vessels](https://vessels.app) —
|
|
3
|
+
Node.js SDK for [Vessels](https://vessels-two.vercel.app) — let your agent reach you.
|
|
4
|
+
|
|
5
|
+
Vessels is the communication layer between AI agents and their human operators. Your agent pushes structured messages to a vessel; the human responds via the web or mobile app; your agent receives the answer via polling or webhooks.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install
|
|
10
|
+
npm install vessels-sdk
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### Push a message with a card and approval interaction
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
-
import { Vessels } from '
|
|
16
|
+
import { Vessels } from 'vessels-sdk';
|
|
17
17
|
|
|
18
18
|
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const { messageId, vesselId } = await vessels.push({
|
|
21
|
+
vessel: 'booking-123',
|
|
22
|
+
vesselTitle: 'Sarah Martinez — Saturday Booking',
|
|
23
23
|
message: 'New booking request received.',
|
|
24
24
|
card: {
|
|
25
25
|
title: 'Booking Details',
|
|
@@ -34,17 +34,299 @@ const response = await vessels.push({
|
|
|
34
34
|
approveLabel: 'Confirm',
|
|
35
35
|
rejectLabel: 'Decline',
|
|
36
36
|
}),
|
|
37
|
+
vesselStatus: 'waiting',
|
|
37
38
|
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Get your API key from Settings → API Keys in the [Vessels app](https://vessels-two.vercel.app), or via the CLI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g vessels
|
|
45
|
+
vessels login
|
|
46
|
+
vessels keys create
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## API reference
|
|
52
|
+
|
|
53
|
+
### `new Vessels(config)`
|
|
54
|
+
|
|
55
|
+
| Option | Type | Default | Description |
|
|
56
|
+
|--------|------|---------|-------------|
|
|
57
|
+
| `apiKey` | `string` | required | Your `vsl_` prefixed API key |
|
|
58
|
+
| `baseUrl` | `string` | `https://vessels-two.vercel.app` | Override for local dev or self-hosted |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### `vessels.push(payload)`
|
|
63
|
+
|
|
64
|
+
Push a message to a vessel. Creates the vessel if it doesn't exist yet.
|
|
65
|
+
|
|
66
|
+
Returns `{ ok: true, messageId: string, vesselId: string, createdAt: string }`.
|
|
67
|
+
|
|
68
|
+
**Full payload:**
|
|
69
|
+
|
|
70
|
+
| Field | Type | Description |
|
|
71
|
+
|-------|------|-------------|
|
|
72
|
+
| `message` | `string` | **Required.** The message text. |
|
|
73
|
+
| `vessel` | `string` | Your external ID for this vessel (e.g. `booking-123`). Creates the vessel on first use. |
|
|
74
|
+
| `vesselTitle` | `string` | Human-readable name shown in the vessel list. Set on creation; updates on subsequent pushes. |
|
|
75
|
+
| `vesselStatus` | `'active' \| 'waiting' \| 'resolved'` | Status badge shown in the vessel list. `waiting` = amber, `resolved` = green. |
|
|
76
|
+
| `labels` | `string[]` | Tags for filtering in the dashboard. Max 10, 50 chars each. Replaces the existing set on every push — send all labels you want, not just new ones. |
|
|
77
|
+
| `metadata` | `object` | Arbitrary JSON stored on the vessel, passed through in webhook callbacks. |
|
|
78
|
+
| `card` | `Card` | Structured key-value info attached to this message. `{ title: string, fields: [{ label, value }] }` |
|
|
79
|
+
| `interaction` | `Interaction` | Interactive prompt for the human (one of 5 types — see helpers below). Max one per message; immutable after the human responds. |
|
|
80
|
+
| `pinCard` | `Card \| null` | Persistent card pinned to the vessel header. Always visible above the message stream. Replaces any existing pinned card. Pass `null` to clear. |
|
|
81
|
+
| `attachments` | `Attachment[]` | Images or files to show in the message. Max 10. You host the files; Vessels renders them. `[{ type: 'image' \| 'file', url: string, filename?: string }]` |
|
|
82
|
+
| `suggestions` | `string[]` | Quick-reply chips shown below the message. Max 5. Tapping fills the text input. Disappear after the user sends any message. |
|
|
83
|
+
| `previewUrl` | `string` | URL used by `confirm_preview` interactions. |
|
|
84
|
+
| `notify` | `boolean` | Whether to send a push notification. Default `true`. |
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
await vessels.push({
|
|
88
|
+
vessel: 'booking-123',
|
|
89
|
+
vesselTitle: 'Sarah Martinez',
|
|
90
|
+
message: 'Ready for review.',
|
|
91
|
+
vesselStatus: 'waiting',
|
|
92
|
+
labels: ['golf', 'vip'],
|
|
93
|
+
metadata: { bookingRef: 'BK-001' },
|
|
94
|
+
card: {
|
|
95
|
+
title: 'Booking',
|
|
96
|
+
fields: [{ label: 'Date', value: 'Saturday 14 June' }],
|
|
97
|
+
},
|
|
98
|
+
pinCard: {
|
|
99
|
+
title: 'Current Status',
|
|
100
|
+
fields: [{ label: 'Status', value: 'Awaiting confirmation' }],
|
|
101
|
+
},
|
|
102
|
+
attachments: [
|
|
103
|
+
{ type: 'image', url: 'https://example.com/photo.jpg' },
|
|
104
|
+
{ type: 'file', url: 'https://example.com/report.pdf', filename: 'Report.pdf' },
|
|
105
|
+
],
|
|
106
|
+
suggestions: ['Looks good', 'Need changes'],
|
|
107
|
+
interaction: vessels.approval({ prompt: 'Approve?' }),
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `vessels.pushMany(payload)`
|
|
114
|
+
|
|
115
|
+
Push the same message to multiple vessels at once. Max 100 vessels per call. Each vessel gets its own independent copy — interactions are responded to individually.
|
|
116
|
+
|
|
117
|
+
Returns `{ ok: true, results: Array<{ vessel, messageId, vesselId, error? }> }`.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const { results } = await vessels.pushMany({
|
|
121
|
+
vessels: ['booking-101', 'booking-102', 'booking-103'],
|
|
122
|
+
message: 'The Windmill Course is closed this Saturday due to maintenance.',
|
|
123
|
+
interaction: vessels.approval({
|
|
124
|
+
prompt: 'Send cancellation email to this client?',
|
|
125
|
+
approveLabel: 'Send',
|
|
126
|
+
rejectLabel: 'Skip',
|
|
127
|
+
}),
|
|
128
|
+
vesselStatus: 'waiting',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
for (const r of results) {
|
|
132
|
+
if (r.error) console.error(`Failed for ${r.vessel}:`, r.error);
|
|
133
|
+
else console.log(`Pushed to ${r.vessel} — message ${r.messageId}`);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`pushMany` accepts the same fields as `push`, except `vessel` and `vesselTitle` are replaced by `vessels` (an array of external IDs). Vessel titles are not settable via `pushMany` — use per-vessel `push` calls for that.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `vessels.editMessage(messageId, patch)`
|
|
142
|
+
|
|
143
|
+
Edit an existing agent-sent message in place. The message re-renders via Supabase Realtime without reloading.
|
|
144
|
+
|
|
145
|
+
Updatable fields: `content`, `card`, `attachments`, `suggestions`. Interactions are immutable — create a new message to re-ask.
|
|
146
|
+
|
|
147
|
+
Only agent-sourced messages can be edited. Returns `{ ok: true }`.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const { messageId } = await vessels.push({
|
|
151
|
+
vessel: 'batch-job-1',
|
|
152
|
+
message: 'Processing bookings... 0 of 50 complete',
|
|
153
|
+
card: { title: 'Progress', fields: [{ label: 'Progress', value: '0 / 50' }] },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Update as work proceeds:
|
|
157
|
+
await vessels.editMessage(messageId, {
|
|
158
|
+
content: 'Processing bookings... 24 of 50 complete',
|
|
159
|
+
card: { title: 'Progress', fields: [{ label: 'Progress', value: '24 / 50' }] },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Final update:
|
|
163
|
+
await vessels.editMessage(messageId, {
|
|
164
|
+
content: 'All 50 bookings processed.',
|
|
165
|
+
card: {
|
|
166
|
+
title: 'Batch Complete',
|
|
167
|
+
fields: [{ label: 'Processed', value: '50' }, { label: 'Errors', value: '0' }],
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### Interaction helpers
|
|
175
|
+
|
|
176
|
+
All five helpers return a typed interaction object to pass as `payload.interaction`. There is a fixed set of five types — no custom types.
|
|
177
|
+
|
|
178
|
+
#### `vessels.approval(opts)`
|
|
179
|
+
|
|
180
|
+
A yes/no decision. Optionally require a reason on rejection.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
vessels.approval({
|
|
184
|
+
prompt: 'Send the invoice?',
|
|
185
|
+
approveLabel: 'Send', // default: 'Approve'
|
|
186
|
+
rejectLabel: 'Cancel', // default: 'Reject'
|
|
187
|
+
reasonRequired: false, // if true, rejection requires a reason
|
|
188
|
+
metadata: { invoiceId: '42' }, // returned in poll event and webhook
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Response shape: `{ action: 'approved' | 'rejected', reason?: string }`
|
|
193
|
+
|
|
194
|
+
#### `vessels.choice(opts)`
|
|
195
|
+
|
|
196
|
+
Pick one option from a list. Optionally allow a free-text custom value.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
vessels.choice({
|
|
200
|
+
prompt: 'Which time slot works?',
|
|
201
|
+
options: [
|
|
202
|
+
{ id: '9am', label: '9:00 AM' },
|
|
203
|
+
{ id: '2pm', label: '2:00 PM' },
|
|
204
|
+
{ id: '5pm', label: '5:00 PM' },
|
|
205
|
+
],
|
|
206
|
+
allowCustom: true,
|
|
207
|
+
customPlaceholder: 'Type a different time...',
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Response shape: `{ selected: string, customValue?: string | null }`
|
|
212
|
+
|
|
213
|
+
#### `vessels.checklist(opts)`
|
|
214
|
+
|
|
215
|
+
Pick one or more items from a list.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
vessels.checklist({
|
|
219
|
+
prompt: 'Which documents are needed?',
|
|
220
|
+
options: [
|
|
221
|
+
{ id: 'id', label: 'Photo ID', checked: true },
|
|
222
|
+
{ id: 'proof', label: 'Proof of address' },
|
|
223
|
+
{ id: 'contract', label: 'Signed contract' },
|
|
224
|
+
],
|
|
225
|
+
minSelections: 1,
|
|
226
|
+
submitLabel: 'Confirm selection',
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Response shape: `{ selected: string[] }` — array of selected option IDs.
|
|
38
231
|
|
|
39
|
-
|
|
40
|
-
|
|
232
|
+
#### `vessels.textInput(opts)`
|
|
233
|
+
|
|
234
|
+
Free-form text from the human.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
vessels.textInput({
|
|
238
|
+
prompt: 'Why is the booking being cancelled?',
|
|
239
|
+
placeholder: 'Enter reason...',
|
|
240
|
+
multiline: true,
|
|
241
|
+
submitLabel: 'Submit',
|
|
242
|
+
})
|
|
41
243
|
```
|
|
42
244
|
|
|
43
|
-
|
|
245
|
+
Response shape: `{ text: string }`
|
|
246
|
+
|
|
247
|
+
#### `vessels.confirmPreview(opts)`
|
|
248
|
+
|
|
249
|
+
Approve or reject after reviewing an external preview (draft email, document, image).
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
vessels.confirmPreview({
|
|
253
|
+
prompt: 'Review the draft email before sending.',
|
|
254
|
+
previewUrl: 'https://your-app.com/drafts/123',
|
|
255
|
+
previewLabel: 'View draft',
|
|
256
|
+
approveLabel: 'Send',
|
|
257
|
+
rejectLabel: 'Edit',
|
|
258
|
+
reasonRequiredOnReject: true,
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Response shape: `{ action: 'approved' | 'rejected', reason?: string }`
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### `vessels.poll(options?)`
|
|
267
|
+
|
|
268
|
+
Fetch pending events. Uses a server-side cursor per API key — each call returns only new events since the last acknowledged poll.
|
|
269
|
+
|
|
270
|
+
Returns `{ ok: true, events: PollEvent[], hasMore: boolean }`.
|
|
271
|
+
|
|
272
|
+
| Option | Type | Default | Description |
|
|
273
|
+
|--------|------|---------|-------------|
|
|
274
|
+
| `ack` | `boolean` | `true` | Advance the cursor so these events aren't returned again |
|
|
275
|
+
| `limit` | `number` | `50` | Max events per call |
|
|
276
|
+
| `since` | `string` | — | ISO timestamp — overrides the cursor for this call only |
|
|
277
|
+
|
|
278
|
+
Poll events are normalised to camelCase by the SDK.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const { events, hasMore } = await vessels.poll({ ack: true });
|
|
282
|
+
|
|
283
|
+
for (const event of events) {
|
|
284
|
+
if (event.type === 'interaction.response') {
|
|
285
|
+
// event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input' | 'confirm_preview'
|
|
286
|
+
// event.response — response shape depends on interactionType (see above)
|
|
287
|
+
// event.interactionMetadata — metadata object you passed when creating the interaction, or null
|
|
288
|
+
// event.messageId — UUID of the message containing the interaction
|
|
289
|
+
// event.vessel.externalId — your original vessel string ('booking-123')
|
|
290
|
+
// event.vessel.id — vessel UUID
|
|
291
|
+
// event.vessel.labels — string[] of tags on this vessel
|
|
292
|
+
// event.user — { id, email } of the responding user, or null
|
|
293
|
+
|
|
294
|
+
if (event.interactionType === 'approval' && event.response.action === 'approved') {
|
|
295
|
+
await confirmBooking(event.vessel.externalId);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (event.type === 'message.user') {
|
|
300
|
+
// event.message.content — what the human typed
|
|
301
|
+
// event.vessel.externalId — your original vessel string
|
|
302
|
+
const reply = await generateReply(event.message.content);
|
|
303
|
+
await vessels.push({ vessel: event.vessel.externalId, message: reply });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// For high volume, page through all pending events
|
|
308
|
+
if (hasMore) {
|
|
309
|
+
// Call poll() again immediately
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Polling is a complete alternative to webhooks. Use it to get started, or when you can't receive inbound HTTP.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### `vessels.verifyWebhook(body, signature, webhookSecret)`
|
|
318
|
+
|
|
319
|
+
Verify the `X-Vessels-Signature` header on incoming webhook requests. Uses constant-time HMAC comparison.
|
|
320
|
+
|
|
321
|
+
- `body` — raw request body as a string, **before** `JSON.parse`
|
|
322
|
+
- `signature` — the `X-Vessels-Signature` header value (format: `sha256=<hex>`)
|
|
323
|
+
- `webhookSecret` — the per-endpoint secret shown when you created the webhook in Settings → Webhooks. **Not the same as your API key.**
|
|
324
|
+
|
|
325
|
+
Returns `true` if the signature is valid, `false` otherwise.
|
|
44
326
|
|
|
45
327
|
```typescript
|
|
46
328
|
import express from 'express';
|
|
47
|
-
import { Vessels } from '
|
|
329
|
+
import { Vessels } from 'vessels-sdk';
|
|
48
330
|
|
|
49
331
|
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
|
|
50
332
|
const app = express();
|
|
@@ -52,56 +334,142 @@ const app = express();
|
|
|
52
334
|
app.post(
|
|
53
335
|
'/webhooks/vessels',
|
|
54
336
|
express.raw({ type: 'application/json' }),
|
|
55
|
-
(req, res) => {
|
|
56
|
-
const signature = req.headers['x-vessels-signature'] as string;
|
|
337
|
+
async (req, res) => {
|
|
57
338
|
const body = req.body.toString('utf8');
|
|
339
|
+
const signature = req.headers['x-vessels-signature'] as string;
|
|
58
340
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
341
|
+
const valid = vessels.verifyWebhook(body, signature, process.env.VESSELS_WEBHOOK_SECRET!);
|
|
342
|
+
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
|
|
62
343
|
|
|
63
|
-
const
|
|
344
|
+
const payload = JSON.parse(body);
|
|
345
|
+
// payload.event — 'interaction.response' | 'message.user'
|
|
346
|
+
// Webhook bodies use snake_case (vessel_id, external_id, interaction_type)
|
|
64
347
|
|
|
65
|
-
if (event
|
|
66
|
-
const {
|
|
67
|
-
|
|
68
|
-
|
|
348
|
+
if (payload.event === 'interaction.response') {
|
|
349
|
+
const { interaction_type, response, vessel, metadata } = payload.data;
|
|
350
|
+
// vessel.external_id — your original vessel string
|
|
351
|
+
if (interaction_type === 'approval' && response.action === 'approved') {
|
|
352
|
+
await confirmBooking(vessel.external_id);
|
|
353
|
+
}
|
|
69
354
|
}
|
|
70
355
|
|
|
71
|
-
|
|
356
|
+
if (payload.event === 'message.user') {
|
|
357
|
+
const { content, vessel, context } = payload.data;
|
|
358
|
+
// context — last 10 messages in the vessel, oldest first (convenience, not canonical state)
|
|
359
|
+
const reply = await generateReply(content, context);
|
|
360
|
+
await vessels.push({ vessel: vessel.external_id, message: reply });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
res.json({ ok: true });
|
|
72
364
|
},
|
|
73
365
|
);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Webhook payload shapes** (snake_case — raw HTTP/JSON layer):
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
// interaction.response
|
|
372
|
+
{
|
|
373
|
+
"event": "interaction.response",
|
|
374
|
+
"vessel_id": "uuid",
|
|
375
|
+
"workspace_id": "uuid",
|
|
376
|
+
"data": {
|
|
377
|
+
"message_id": "uuid",
|
|
378
|
+
"interaction_type": "approval",
|
|
379
|
+
"response": { "action": "approved" },
|
|
380
|
+
"response_id": "uuid",
|
|
381
|
+
"metadata": { "invoiceId": "42" },
|
|
382
|
+
"vessel": { "id": "uuid", "external_id": "booking-123", "title": "Sarah Martinez", "metadata": {} }
|
|
383
|
+
},
|
|
384
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
385
|
+
}
|
|
74
386
|
|
|
75
|
-
|
|
387
|
+
// message.user
|
|
388
|
+
{
|
|
389
|
+
"event": "message.user",
|
|
390
|
+
"vessel_id": "uuid",
|
|
391
|
+
"workspace_id": "uuid",
|
|
392
|
+
"data": {
|
|
393
|
+
"message_id": "uuid",
|
|
394
|
+
"content": "I want to reschedule",
|
|
395
|
+
"vessel": { "id": "uuid", "external_id": "booking-123", "title": "Sarah Martinez", "metadata": {} },
|
|
396
|
+
"context": [{ "source": "agent", "content": "...", "created_at": "..." }]
|
|
397
|
+
},
|
|
398
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
399
|
+
}
|
|
76
400
|
```
|
|
77
401
|
|
|
78
|
-
|
|
402
|
+
Retries: 3× on failure with backoff (1s, 10s, 60s). All deliveries logged in Settings → Logs.
|
|
79
403
|
|
|
80
|
-
|
|
404
|
+
---
|
|
81
405
|
|
|
82
|
-
|
|
83
|
-
|--------|------|---------|-------------|
|
|
84
|
-
| `apiKey` | `string` | required | Your `vsl_` prefixed API key |
|
|
85
|
-
| `baseUrl` | `string` | `https://vessels.app` | Override for self-hosted or local dev |
|
|
406
|
+
## Error handling
|
|
86
407
|
|
|
87
|
-
|
|
408
|
+
Three typed error classes are exported alongside `Vessels`:
|
|
88
409
|
|
|
89
|
-
|
|
410
|
+
| Class | HTTP status | Description |
|
|
411
|
+
|-------|-------------|-------------|
|
|
412
|
+
| `VesselsAuthError` | 401 | Invalid or revoked API key |
|
|
413
|
+
| `VesselsValidationError` | 400 | Bad request payload. Check `.details` for field-level errors. |
|
|
414
|
+
| `VesselsRateLimitError` | 429 | Rate limit exceeded. Check `.retryAfter` (seconds) before retrying. |
|
|
90
415
|
|
|
91
|
-
|
|
416
|
+
```typescript
|
|
417
|
+
import { Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError } from 'vessels-sdk';
|
|
92
418
|
|
|
93
|
-
|
|
419
|
+
try {
|
|
420
|
+
await vessels.push({ vessel: 'booking-123', message: 'hello' });
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (err instanceof VesselsAuthError) {
|
|
423
|
+
console.error('Check your API key — it may be revoked');
|
|
424
|
+
} else if (err instanceof VesselsRateLimitError) {
|
|
425
|
+
console.error(`Rate limited — retry after ${err.retryAfter}s`);
|
|
426
|
+
} else if (err instanceof VesselsValidationError) {
|
|
427
|
+
console.error('Bad payload:', err.details);
|
|
428
|
+
} else {
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Naming conventions
|
|
437
|
+
|
|
438
|
+
The SDK and push payload use **camelCase** (JavaScript convention): `vesselTitle`, `vesselStatus`, `pinCard`, `vesselId`, `messageId`.
|
|
94
439
|
|
|
95
|
-
|
|
440
|
+
Webhook POST bodies use **snake_case** (raw HTTP/JSON layer): `vessel_id`, `external_id`, `interaction_type`.
|
|
441
|
+
|
|
442
|
+
Poll events are normalised to **camelCase** by the SDK: `event.vessel.externalId`, `event.interactionType`, `event.messageId`.
|
|
443
|
+
|
|
444
|
+
Rule of thumb: anything you **write** (push calls, SDK methods) is camelCase. Anything you **read** from a raw webhook POST body is snake_case.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## TypeScript types
|
|
449
|
+
|
|
450
|
+
All types used by the SDK are exported directly from `vessels-sdk`:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import type {
|
|
454
|
+
PushPayload,
|
|
455
|
+
PushManyPayload,
|
|
456
|
+
MessagePatch,
|
|
457
|
+
Attachment,
|
|
458
|
+
Interaction,
|
|
459
|
+
Card,
|
|
460
|
+
ApprovalInteraction,
|
|
461
|
+
ChoiceInteraction,
|
|
462
|
+
ChecklistInteraction,
|
|
463
|
+
TextInputInteraction,
|
|
464
|
+
ConfirmPreviewInteraction,
|
|
465
|
+
} from 'vessels-sdk';
|
|
466
|
+
```
|
|
96
467
|
|
|
97
|
-
|
|
98
|
-
|--------|-----------|
|
|
99
|
-
| `vessels.approval(opts)` | Yes/no decision |
|
|
100
|
-
| `vessels.choice(opts)` | Pick one option |
|
|
101
|
-
| `vessels.checklist(opts)` | Pick multiple options |
|
|
102
|
-
| `vessels.textInput(opts)` | Free-form text |
|
|
103
|
-
| `vessels.confirmPreview(opts)` | Approve with external preview link |
|
|
468
|
+
---
|
|
104
469
|
|
|
105
|
-
|
|
470
|
+
## Links
|
|
106
471
|
|
|
107
|
-
|
|
472
|
+
- Dashboard: [https://vessels-two.vercel.app](https://vessels-two.vercel.app)
|
|
473
|
+
- Full integration reference: [https://vessels-two.vercel.app/docs](https://vessels-two.vercel.app/docs)
|
|
474
|
+
- CLI: `npm install -g vessels`
|
|
475
|
+
- npm: [https://www.npmjs.com/package/vessels-sdk](https://www.npmjs.com/package/vessels-sdk)
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var crypto = require('crypto');
|
|
4
|
-
|
|
5
3
|
// src/index.ts
|
|
4
|
+
var AgentActivityTypes = {
|
|
5
|
+
thinking: "thinking",
|
|
6
|
+
searching: "searching",
|
|
7
|
+
toolUse: "tool_use",
|
|
8
|
+
browsing: "browsing",
|
|
9
|
+
processing: "processing"
|
|
10
|
+
};
|
|
6
11
|
var VesselsAuthError = class extends Error {
|
|
7
12
|
constructor(message) {
|
|
8
13
|
super(message);
|
|
@@ -28,12 +33,37 @@ var VesselsRateLimitError = class extends Error {
|
|
|
28
33
|
var Vessels = class {
|
|
29
34
|
apiKey;
|
|
30
35
|
baseUrl;
|
|
36
|
+
_debug;
|
|
31
37
|
constructor(config) {
|
|
32
38
|
this.apiKey = config.apiKey;
|
|
33
39
|
this.baseUrl = config.baseUrl?.replace(/\/$/, "") ?? "https://vessels-two.vercel.app";
|
|
40
|
+
this._debug = config.debug ?? false;
|
|
41
|
+
}
|
|
42
|
+
async _fetch(url, init) {
|
|
43
|
+
if (this._debug) {
|
|
44
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
45
|
+
console.log(`[Vessels] \u2192 ${method} ${url}`);
|
|
46
|
+
if (init.body) {
|
|
47
|
+
try {
|
|
48
|
+
console.log("[Vessels] body:", JSON.parse(init.body));
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const res = await fetch(url, init);
|
|
54
|
+
if (this._debug) {
|
|
55
|
+
const clone = res.clone();
|
|
56
|
+
try {
|
|
57
|
+
const body = await clone.json();
|
|
58
|
+
console.log(`[Vessels] \u2190 ${res.status}`, body);
|
|
59
|
+
} catch {
|
|
60
|
+
console.log(`[Vessels] \u2190 ${res.status}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return res;
|
|
34
64
|
}
|
|
35
65
|
async push(payload) {
|
|
36
|
-
const res = await
|
|
66
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
|
|
37
67
|
method: "POST",
|
|
38
68
|
headers: {
|
|
39
69
|
"Content-Type": "application/json",
|
|
@@ -54,7 +84,7 @@ var Vessels = class {
|
|
|
54
84
|
};
|
|
55
85
|
}
|
|
56
86
|
async pushMany(payload) {
|
|
57
|
-
const res = await
|
|
87
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
|
|
58
88
|
method: "POST",
|
|
59
89
|
headers: {
|
|
60
90
|
"Content-Type": "application/json",
|
|
@@ -78,7 +108,7 @@ var Vessels = class {
|
|
|
78
108
|
};
|
|
79
109
|
}
|
|
80
110
|
async editMessage(messageId, patch) {
|
|
81
|
-
const res = await
|
|
111
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/messages/${messageId}`, {
|
|
82
112
|
method: "PATCH",
|
|
83
113
|
headers: {
|
|
84
114
|
"Content-Type": "application/json",
|
|
@@ -116,7 +146,7 @@ var Vessels = class {
|
|
|
116
146
|
if (since) params.set("since", since);
|
|
117
147
|
params.set("limit", String(limit));
|
|
118
148
|
params.set("ack", String(ack));
|
|
119
|
-
const res = await
|
|
149
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/poll?${params}`, {
|
|
120
150
|
headers: { "Authorization": `Bearer ${this.apiKey}` }
|
|
121
151
|
});
|
|
122
152
|
const data = await res.json();
|
|
@@ -157,19 +187,31 @@ var Vessels = class {
|
|
|
157
187
|
// body: raw request body string (before JSON.parse)
|
|
158
188
|
// signature: X-Vessels-Signature header value
|
|
159
189
|
// webhookSecret: the secret shown when you created the webhook endpoint in Settings
|
|
160
|
-
verifyWebhook(body, signature, webhookSecret) {
|
|
190
|
+
async verifyWebhook(body, signature, webhookSecret) {
|
|
161
191
|
if (!signature.startsWith("sha256=")) return false;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
try {
|
|
193
|
+
const enc = new TextEncoder();
|
|
194
|
+
const hexSig = signature.slice(7);
|
|
195
|
+
if (hexSig.length % 2 !== 0) return false;
|
|
196
|
+
const sigBytes = new Uint8Array(hexSig.length / 2);
|
|
197
|
+
for (let i = 0; i < sigBytes.length; i++) {
|
|
198
|
+
sigBytes[i] = parseInt(hexSig.slice(i * 2, i * 2 + 2), 16);
|
|
199
|
+
}
|
|
200
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
201
|
+
"raw",
|
|
202
|
+
enc.encode(webhookSecret),
|
|
203
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
204
|
+
false,
|
|
205
|
+
["verify"]
|
|
206
|
+
);
|
|
207
|
+
return await globalThis.crypto.subtle.verify("HMAC", key, sigBytes, enc.encode(body));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
168
210
|
}
|
|
169
|
-
return diff === 0;
|
|
170
211
|
}
|
|
171
212
|
};
|
|
172
213
|
|
|
214
|
+
exports.AgentActivityTypes = AgentActivityTypes;
|
|
173
215
|
exports.Vessels = Vessels;
|
|
174
216
|
exports.VesselsAuthError = VesselsAuthError;
|
|
175
217
|
exports.VesselsRateLimitError = VesselsRateLimitError;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as _vessels_types from '@vessels/types';
|
|
2
|
-
export { ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
2
|
+
export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
3
3
|
|
|
4
|
+
declare const AgentActivityTypes: {
|
|
5
|
+
readonly thinking: "thinking";
|
|
6
|
+
readonly searching: "searching";
|
|
7
|
+
readonly toolUse: "tool_use";
|
|
8
|
+
readonly browsing: "browsing";
|
|
9
|
+
readonly processing: "processing";
|
|
10
|
+
};
|
|
4
11
|
declare class VesselsAuthError extends Error {
|
|
5
12
|
constructor(message: string);
|
|
6
13
|
}
|
|
@@ -15,6 +22,7 @@ declare class VesselsRateLimitError extends Error {
|
|
|
15
22
|
interface VesselsConfig {
|
|
16
23
|
apiKey: string;
|
|
17
24
|
baseUrl?: string;
|
|
25
|
+
debug?: boolean;
|
|
18
26
|
}
|
|
19
27
|
interface PushResponse {
|
|
20
28
|
ok: true;
|
|
@@ -76,7 +84,9 @@ interface PollResponse {
|
|
|
76
84
|
declare class Vessels {
|
|
77
85
|
private apiKey;
|
|
78
86
|
private baseUrl;
|
|
87
|
+
private _debug;
|
|
79
88
|
constructor(config: VesselsConfig);
|
|
89
|
+
private _fetch;
|
|
80
90
|
push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
|
|
81
91
|
pushMany(payload: _vessels_types.PushManyPayload): Promise<PushManyResult>;
|
|
82
92
|
editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
|
|
@@ -127,7 +137,7 @@ declare class Vessels {
|
|
|
127
137
|
metadata?: Record<string, unknown>;
|
|
128
138
|
}): _vessels_types.ConfirmPreviewInteraction;
|
|
129
139
|
poll(options?: PollOptions): Promise<PollResponse>;
|
|
130
|
-
verifyWebhook(body: string, signature: string, webhookSecret: string): boolean
|
|
140
|
+
verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
export { type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
|
143
|
+
export { AgentActivityTypes, type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as _vessels_types from '@vessels/types';
|
|
2
|
-
export { ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
2
|
+
export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
3
3
|
|
|
4
|
+
declare const AgentActivityTypes: {
|
|
5
|
+
readonly thinking: "thinking";
|
|
6
|
+
readonly searching: "searching";
|
|
7
|
+
readonly toolUse: "tool_use";
|
|
8
|
+
readonly browsing: "browsing";
|
|
9
|
+
readonly processing: "processing";
|
|
10
|
+
};
|
|
4
11
|
declare class VesselsAuthError extends Error {
|
|
5
12
|
constructor(message: string);
|
|
6
13
|
}
|
|
@@ -15,6 +22,7 @@ declare class VesselsRateLimitError extends Error {
|
|
|
15
22
|
interface VesselsConfig {
|
|
16
23
|
apiKey: string;
|
|
17
24
|
baseUrl?: string;
|
|
25
|
+
debug?: boolean;
|
|
18
26
|
}
|
|
19
27
|
interface PushResponse {
|
|
20
28
|
ok: true;
|
|
@@ -76,7 +84,9 @@ interface PollResponse {
|
|
|
76
84
|
declare class Vessels {
|
|
77
85
|
private apiKey;
|
|
78
86
|
private baseUrl;
|
|
87
|
+
private _debug;
|
|
79
88
|
constructor(config: VesselsConfig);
|
|
89
|
+
private _fetch;
|
|
80
90
|
push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
|
|
81
91
|
pushMany(payload: _vessels_types.PushManyPayload): Promise<PushManyResult>;
|
|
82
92
|
editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
|
|
@@ -127,7 +137,7 @@ declare class Vessels {
|
|
|
127
137
|
metadata?: Record<string, unknown>;
|
|
128
138
|
}): _vessels_types.ConfirmPreviewInteraction;
|
|
129
139
|
poll(options?: PollOptions): Promise<PollResponse>;
|
|
130
|
-
verifyWebhook(body: string, signature: string, webhookSecret: string): boolean
|
|
140
|
+
verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
export { type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
|
143
|
+
export { AgentActivityTypes, type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { createHmac } from 'crypto';
|
|
2
|
-
|
|
3
1
|
// src/index.ts
|
|
2
|
+
var AgentActivityTypes = {
|
|
3
|
+
thinking: "thinking",
|
|
4
|
+
searching: "searching",
|
|
5
|
+
toolUse: "tool_use",
|
|
6
|
+
browsing: "browsing",
|
|
7
|
+
processing: "processing"
|
|
8
|
+
};
|
|
4
9
|
var VesselsAuthError = class extends Error {
|
|
5
10
|
constructor(message) {
|
|
6
11
|
super(message);
|
|
@@ -26,12 +31,37 @@ var VesselsRateLimitError = class extends Error {
|
|
|
26
31
|
var Vessels = class {
|
|
27
32
|
apiKey;
|
|
28
33
|
baseUrl;
|
|
34
|
+
_debug;
|
|
29
35
|
constructor(config) {
|
|
30
36
|
this.apiKey = config.apiKey;
|
|
31
37
|
this.baseUrl = config.baseUrl?.replace(/\/$/, "") ?? "https://vessels-two.vercel.app";
|
|
38
|
+
this._debug = config.debug ?? false;
|
|
39
|
+
}
|
|
40
|
+
async _fetch(url, init) {
|
|
41
|
+
if (this._debug) {
|
|
42
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
43
|
+
console.log(`[Vessels] \u2192 ${method} ${url}`);
|
|
44
|
+
if (init.body) {
|
|
45
|
+
try {
|
|
46
|
+
console.log("[Vessels] body:", JSON.parse(init.body));
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const res = await fetch(url, init);
|
|
52
|
+
if (this._debug) {
|
|
53
|
+
const clone = res.clone();
|
|
54
|
+
try {
|
|
55
|
+
const body = await clone.json();
|
|
56
|
+
console.log(`[Vessels] \u2190 ${res.status}`, body);
|
|
57
|
+
} catch {
|
|
58
|
+
console.log(`[Vessels] \u2190 ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return res;
|
|
32
62
|
}
|
|
33
63
|
async push(payload) {
|
|
34
|
-
const res = await
|
|
64
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
|
|
35
65
|
method: "POST",
|
|
36
66
|
headers: {
|
|
37
67
|
"Content-Type": "application/json",
|
|
@@ -52,7 +82,7 @@ var Vessels = class {
|
|
|
52
82
|
};
|
|
53
83
|
}
|
|
54
84
|
async pushMany(payload) {
|
|
55
|
-
const res = await
|
|
85
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
|
|
56
86
|
method: "POST",
|
|
57
87
|
headers: {
|
|
58
88
|
"Content-Type": "application/json",
|
|
@@ -76,7 +106,7 @@ var Vessels = class {
|
|
|
76
106
|
};
|
|
77
107
|
}
|
|
78
108
|
async editMessage(messageId, patch) {
|
|
79
|
-
const res = await
|
|
109
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/messages/${messageId}`, {
|
|
80
110
|
method: "PATCH",
|
|
81
111
|
headers: {
|
|
82
112
|
"Content-Type": "application/json",
|
|
@@ -114,7 +144,7 @@ var Vessels = class {
|
|
|
114
144
|
if (since) params.set("since", since);
|
|
115
145
|
params.set("limit", String(limit));
|
|
116
146
|
params.set("ack", String(ack));
|
|
117
|
-
const res = await
|
|
147
|
+
const res = await this._fetch(`${this.baseUrl}/api/v1/poll?${params}`, {
|
|
118
148
|
headers: { "Authorization": `Bearer ${this.apiKey}` }
|
|
119
149
|
});
|
|
120
150
|
const data = await res.json();
|
|
@@ -155,17 +185,28 @@ var Vessels = class {
|
|
|
155
185
|
// body: raw request body string (before JSON.parse)
|
|
156
186
|
// signature: X-Vessels-Signature header value
|
|
157
187
|
// webhookSecret: the secret shown when you created the webhook endpoint in Settings
|
|
158
|
-
verifyWebhook(body, signature, webhookSecret) {
|
|
188
|
+
async verifyWebhook(body, signature, webhookSecret) {
|
|
159
189
|
if (!signature.startsWith("sha256=")) return false;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
try {
|
|
191
|
+
const enc = new TextEncoder();
|
|
192
|
+
const hexSig = signature.slice(7);
|
|
193
|
+
if (hexSig.length % 2 !== 0) return false;
|
|
194
|
+
const sigBytes = new Uint8Array(hexSig.length / 2);
|
|
195
|
+
for (let i = 0; i < sigBytes.length; i++) {
|
|
196
|
+
sigBytes[i] = parseInt(hexSig.slice(i * 2, i * 2 + 2), 16);
|
|
197
|
+
}
|
|
198
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
199
|
+
"raw",
|
|
200
|
+
enc.encode(webhookSecret),
|
|
201
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
202
|
+
false,
|
|
203
|
+
["verify"]
|
|
204
|
+
);
|
|
205
|
+
return await globalThis.crypto.subtle.verify("HMAC", key, sigBytes, enc.encode(body));
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
166
208
|
}
|
|
167
|
-
return diff === 0;
|
|
168
209
|
}
|
|
169
210
|
};
|
|
170
211
|
|
|
171
|
-
export { Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError };
|
|
212
|
+
export { AgentActivityTypes, Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError };
|