vessels-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 +107 -0
- package/dist/index.cjs +131 -0
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +126 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @vessels/sdk
|
|
2
|
+
|
|
3
|
+
Node.js SDK for [Vessels](https://vessels.app) — push messages from your agent to your phone and verify webhook callbacks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vessels/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Push a message with a card and approval interaction
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Vessels } from '@vessels/sdk';
|
|
17
|
+
|
|
18
|
+
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
|
|
19
|
+
|
|
20
|
+
const response = await vessels.push({
|
|
21
|
+
thread: 'booking-123',
|
|
22
|
+
threadTitle: 'Sarah Martinez',
|
|
23
|
+
message: 'New booking request received.',
|
|
24
|
+
card: {
|
|
25
|
+
title: 'Booking Details',
|
|
26
|
+
fields: [
|
|
27
|
+
{ label: 'Date', value: 'Saturday 14 June' },
|
|
28
|
+
{ label: 'Time', value: '2:00 PM' },
|
|
29
|
+
{ label: 'Players', value: '4' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
interaction: vessels.approval({
|
|
33
|
+
prompt: 'Confirm this booking?',
|
|
34
|
+
approveLabel: 'Confirm',
|
|
35
|
+
rejectLabel: 'Decline',
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(response.messageId); // the created message ID
|
|
40
|
+
console.log(response.threadId); // the upserted thread ID
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Verify a webhook in an Express handler
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import express from 'express';
|
|
47
|
+
import { Vessels } from '@vessels/sdk';
|
|
48
|
+
|
|
49
|
+
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
|
|
50
|
+
const app = express();
|
|
51
|
+
|
|
52
|
+
app.post(
|
|
53
|
+
'/webhooks/vessels',
|
|
54
|
+
express.raw({ type: 'application/json' }),
|
|
55
|
+
(req, res) => {
|
|
56
|
+
const signature = req.headers['x-vessels-signature'] as string;
|
|
57
|
+
const body = req.body.toString('utf8');
|
|
58
|
+
|
|
59
|
+
if (!vessels.verifyWebhook(body, signature)) {
|
|
60
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const event = JSON.parse(body);
|
|
64
|
+
|
|
65
|
+
if (event.type === 'interaction.response') {
|
|
66
|
+
const { interactionType, response } = event.data;
|
|
67
|
+
console.log(`User responded to ${interactionType}:`, response);
|
|
68
|
+
// e.g. { action: 'approved' } for an approval card
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.status(200).json({ received: true });
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
app.listen(3000);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
### `new Vessels(config)`
|
|
81
|
+
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
|--------|------|---------|-------------|
|
|
84
|
+
| `apiKey` | `string` | required | Your `vsl_` prefixed API key |
|
|
85
|
+
| `baseUrl` | `string` | `https://vessels.app` | Override for self-hosted or local dev |
|
|
86
|
+
|
|
87
|
+
### `vessels.push(payload)`
|
|
88
|
+
|
|
89
|
+
Push a message to a thread. Returns `{ ok, messageId, threadId, createdAt }`.
|
|
90
|
+
|
|
91
|
+
Throws `VesselsAuthError` (401), `VesselsRateLimitError` (429), or `VesselsValidationError` (400) on failure.
|
|
92
|
+
|
|
93
|
+
### Interaction helpers
|
|
94
|
+
|
|
95
|
+
All helpers return a typed interaction object to pass as `payload.interaction`.
|
|
96
|
+
|
|
97
|
+
| Method | Card type |
|
|
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 |
|
|
104
|
+
|
|
105
|
+
### `vessels.verifyWebhook(body, signature)`
|
|
106
|
+
|
|
107
|
+
Verifies the `X-Vessels-Signature` header using HMAC-SHA256. Pass the **raw** request body as a string. Returns `true` if valid.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var VesselsAuthError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "VesselsAuthError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var VesselsValidationError = class extends Error {
|
|
13
|
+
details;
|
|
14
|
+
constructor(message, details) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "VesselsValidationError";
|
|
17
|
+
this.details = details;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var VesselsRateLimitError = class extends Error {
|
|
21
|
+
retryAfter;
|
|
22
|
+
constructor(message, retryAfter) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "VesselsRateLimitError";
|
|
25
|
+
this.retryAfter = retryAfter;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var Vessels = class {
|
|
29
|
+
apiKey;
|
|
30
|
+
baseUrl;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.apiKey = config.apiKey;
|
|
33
|
+
this.baseUrl = config.baseUrl?.replace(/\/$/, "") ?? "https://vessels-two.vercel.app";
|
|
34
|
+
}
|
|
35
|
+
async push(payload) {
|
|
36
|
+
const res = await fetch(`${this.baseUrl}/api/v1/push`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(payload)
|
|
43
|
+
});
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
|
|
46
|
+
if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
|
|
47
|
+
if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
|
|
48
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
messageId: data.message_id,
|
|
52
|
+
vesselId: data.vessel_id,
|
|
53
|
+
createdAt: data.created_at
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Interaction helpers
|
|
57
|
+
approval(opts) {
|
|
58
|
+
return { type: "approval", ...opts };
|
|
59
|
+
}
|
|
60
|
+
choice(opts) {
|
|
61
|
+
return { type: "choice", ...opts };
|
|
62
|
+
}
|
|
63
|
+
checklist(opts) {
|
|
64
|
+
return { type: "checklist", ...opts };
|
|
65
|
+
}
|
|
66
|
+
textInput(opts) {
|
|
67
|
+
return { type: "text_input", ...opts };
|
|
68
|
+
}
|
|
69
|
+
confirmPreview(opts) {
|
|
70
|
+
return { type: "confirm_preview", ...opts };
|
|
71
|
+
}
|
|
72
|
+
async poll(options = {}) {
|
|
73
|
+
const { since, limit = 50, ack = true } = options;
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
if (since) params.set("since", since);
|
|
76
|
+
params.set("limit", String(limit));
|
|
77
|
+
params.set("ack", String(ack));
|
|
78
|
+
const res = await fetch(`${this.baseUrl}/api/v1/poll?${params}`, {
|
|
79
|
+
headers: { "Authorization": `Bearer ${this.apiKey}` }
|
|
80
|
+
});
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
|
|
83
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
84
|
+
const events = (data.events ?? []).map((e) => {
|
|
85
|
+
const vessel = {
|
|
86
|
+
id: e.vessel?.id,
|
|
87
|
+
externalId: e.vessel?.external_id ?? null,
|
|
88
|
+
title: e.vessel?.title ?? null,
|
|
89
|
+
metadata: e.vessel?.metadata ?? {}
|
|
90
|
+
};
|
|
91
|
+
if (e.type === "interaction.response") {
|
|
92
|
+
return {
|
|
93
|
+
id: e.id,
|
|
94
|
+
type: "interaction.response",
|
|
95
|
+
timestamp: e.timestamp,
|
|
96
|
+
vessel,
|
|
97
|
+
messageId: e.message_id,
|
|
98
|
+
interactionType: e.interaction_type,
|
|
99
|
+
response: e.response,
|
|
100
|
+
user: e.user ?? null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id: e.id,
|
|
105
|
+
type: "message.user",
|
|
106
|
+
timestamp: e.timestamp,
|
|
107
|
+
vessel,
|
|
108
|
+
message: { id: e.message?.id, content: e.message?.content ?? null }
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, events, hasMore: data.has_more ?? false };
|
|
112
|
+
}
|
|
113
|
+
// Webhook verification — call this in your webhook handler
|
|
114
|
+
// body: raw request body string, signature: X-Vessels-Signature header value
|
|
115
|
+
verifyWebhook(body, signature) {
|
|
116
|
+
if (!signature.startsWith("sha256=")) return false;
|
|
117
|
+
const expected = crypto.createHmac("sha256", this.apiKey).update(body).digest("hex");
|
|
118
|
+
const received = signature.slice(7);
|
|
119
|
+
if (expected.length !== received.length) return false;
|
|
120
|
+
let diff = 0;
|
|
121
|
+
for (let i = 0; i < expected.length; i++) {
|
|
122
|
+
diff |= expected.charCodeAt(i) ^ received.charCodeAt(i);
|
|
123
|
+
}
|
|
124
|
+
return diff === 0;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
exports.Vessels = Vessels;
|
|
129
|
+
exports.VesselsAuthError = VesselsAuthError;
|
|
130
|
+
exports.VesselsRateLimitError = VesselsRateLimitError;
|
|
131
|
+
exports.VesselsValidationError = VesselsValidationError;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as _vessels_types from '@vessels/types';
|
|
2
|
+
export { ApprovalInteraction, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
3
|
+
|
|
4
|
+
declare class VesselsAuthError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
declare class VesselsValidationError extends Error {
|
|
8
|
+
details?: unknown;
|
|
9
|
+
constructor(message: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
declare class VesselsRateLimitError extends Error {
|
|
12
|
+
retryAfter?: number;
|
|
13
|
+
constructor(message: string, retryAfter?: number);
|
|
14
|
+
}
|
|
15
|
+
interface VesselsConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
interface PushResponse {
|
|
20
|
+
ok: true;
|
|
21
|
+
messageId: string;
|
|
22
|
+
vesselId: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
interface PollOptions {
|
|
26
|
+
since?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
ack?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface VesselContext {
|
|
31
|
+
id: string;
|
|
32
|
+
externalId: string | null;
|
|
33
|
+
title: string | null;
|
|
34
|
+
metadata: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
interface InteractionResponseEvent {
|
|
37
|
+
id: string;
|
|
38
|
+
type: 'interaction.response';
|
|
39
|
+
timestamp: string;
|
|
40
|
+
vessel: VesselContext;
|
|
41
|
+
messageId: string;
|
|
42
|
+
interactionType: string;
|
|
43
|
+
response: Record<string, unknown>;
|
|
44
|
+
user: {
|
|
45
|
+
id: string;
|
|
46
|
+
email: string;
|
|
47
|
+
} | null;
|
|
48
|
+
}
|
|
49
|
+
interface UserMessageEvent {
|
|
50
|
+
id: string;
|
|
51
|
+
type: 'message.user';
|
|
52
|
+
timestamp: string;
|
|
53
|
+
vessel: VesselContext;
|
|
54
|
+
message: {
|
|
55
|
+
id: string;
|
|
56
|
+
content: string | null;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
type PollEvent = InteractionResponseEvent | UserMessageEvent;
|
|
60
|
+
interface PollResponse {
|
|
61
|
+
ok: true;
|
|
62
|
+
events: PollEvent[];
|
|
63
|
+
hasMore: boolean;
|
|
64
|
+
}
|
|
65
|
+
declare class Vessels {
|
|
66
|
+
private apiKey;
|
|
67
|
+
private baseUrl;
|
|
68
|
+
constructor(config: VesselsConfig);
|
|
69
|
+
push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
|
|
70
|
+
approval(opts: {
|
|
71
|
+
prompt: string;
|
|
72
|
+
approveLabel?: string;
|
|
73
|
+
rejectLabel?: string;
|
|
74
|
+
reasonRequired?: boolean;
|
|
75
|
+
}): _vessels_types.ApprovalInteraction;
|
|
76
|
+
choice(opts: {
|
|
77
|
+
prompt: string;
|
|
78
|
+
options: Array<{
|
|
79
|
+
id: string;
|
|
80
|
+
label: string;
|
|
81
|
+
}>;
|
|
82
|
+
allowCustom?: boolean;
|
|
83
|
+
customPlaceholder?: string;
|
|
84
|
+
}): _vessels_types.ChoiceInteraction;
|
|
85
|
+
checklist(opts: {
|
|
86
|
+
prompt: string;
|
|
87
|
+
options: Array<{
|
|
88
|
+
id: string;
|
|
89
|
+
label: string;
|
|
90
|
+
checked?: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
minSelections?: number;
|
|
93
|
+
submitLabel?: string;
|
|
94
|
+
}): _vessels_types.ChecklistInteraction;
|
|
95
|
+
textInput(opts: {
|
|
96
|
+
prompt: string;
|
|
97
|
+
placeholder?: string;
|
|
98
|
+
multiline?: boolean;
|
|
99
|
+
submitLabel?: string;
|
|
100
|
+
}): _vessels_types.TextInputInteraction;
|
|
101
|
+
confirmPreview(opts: {
|
|
102
|
+
prompt: string;
|
|
103
|
+
previewUrl: string;
|
|
104
|
+
previewLabel?: string;
|
|
105
|
+
approveLabel?: string;
|
|
106
|
+
rejectLabel?: string;
|
|
107
|
+
reasonRequiredOnReject?: boolean;
|
|
108
|
+
}): _vessels_types.ConfirmPreviewInteraction;
|
|
109
|
+
poll(options?: PollOptions): Promise<PollResponse>;
|
|
110
|
+
verifyWebhook(body: string, signature: string): boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as _vessels_types from '@vessels/types';
|
|
2
|
+
export { ApprovalInteraction, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, PushPayload, TextInputInteraction } from '@vessels/types';
|
|
3
|
+
|
|
4
|
+
declare class VesselsAuthError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
declare class VesselsValidationError extends Error {
|
|
8
|
+
details?: unknown;
|
|
9
|
+
constructor(message: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
declare class VesselsRateLimitError extends Error {
|
|
12
|
+
retryAfter?: number;
|
|
13
|
+
constructor(message: string, retryAfter?: number);
|
|
14
|
+
}
|
|
15
|
+
interface VesselsConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
interface PushResponse {
|
|
20
|
+
ok: true;
|
|
21
|
+
messageId: string;
|
|
22
|
+
vesselId: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
interface PollOptions {
|
|
26
|
+
since?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
ack?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface VesselContext {
|
|
31
|
+
id: string;
|
|
32
|
+
externalId: string | null;
|
|
33
|
+
title: string | null;
|
|
34
|
+
metadata: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
interface InteractionResponseEvent {
|
|
37
|
+
id: string;
|
|
38
|
+
type: 'interaction.response';
|
|
39
|
+
timestamp: string;
|
|
40
|
+
vessel: VesselContext;
|
|
41
|
+
messageId: string;
|
|
42
|
+
interactionType: string;
|
|
43
|
+
response: Record<string, unknown>;
|
|
44
|
+
user: {
|
|
45
|
+
id: string;
|
|
46
|
+
email: string;
|
|
47
|
+
} | null;
|
|
48
|
+
}
|
|
49
|
+
interface UserMessageEvent {
|
|
50
|
+
id: string;
|
|
51
|
+
type: 'message.user';
|
|
52
|
+
timestamp: string;
|
|
53
|
+
vessel: VesselContext;
|
|
54
|
+
message: {
|
|
55
|
+
id: string;
|
|
56
|
+
content: string | null;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
type PollEvent = InteractionResponseEvent | UserMessageEvent;
|
|
60
|
+
interface PollResponse {
|
|
61
|
+
ok: true;
|
|
62
|
+
events: PollEvent[];
|
|
63
|
+
hasMore: boolean;
|
|
64
|
+
}
|
|
65
|
+
declare class Vessels {
|
|
66
|
+
private apiKey;
|
|
67
|
+
private baseUrl;
|
|
68
|
+
constructor(config: VesselsConfig);
|
|
69
|
+
push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
|
|
70
|
+
approval(opts: {
|
|
71
|
+
prompt: string;
|
|
72
|
+
approveLabel?: string;
|
|
73
|
+
rejectLabel?: string;
|
|
74
|
+
reasonRequired?: boolean;
|
|
75
|
+
}): _vessels_types.ApprovalInteraction;
|
|
76
|
+
choice(opts: {
|
|
77
|
+
prompt: string;
|
|
78
|
+
options: Array<{
|
|
79
|
+
id: string;
|
|
80
|
+
label: string;
|
|
81
|
+
}>;
|
|
82
|
+
allowCustom?: boolean;
|
|
83
|
+
customPlaceholder?: string;
|
|
84
|
+
}): _vessels_types.ChoiceInteraction;
|
|
85
|
+
checklist(opts: {
|
|
86
|
+
prompt: string;
|
|
87
|
+
options: Array<{
|
|
88
|
+
id: string;
|
|
89
|
+
label: string;
|
|
90
|
+
checked?: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
minSelections?: number;
|
|
93
|
+
submitLabel?: string;
|
|
94
|
+
}): _vessels_types.ChecklistInteraction;
|
|
95
|
+
textInput(opts: {
|
|
96
|
+
prompt: string;
|
|
97
|
+
placeholder?: string;
|
|
98
|
+
multiline?: boolean;
|
|
99
|
+
submitLabel?: string;
|
|
100
|
+
}): _vessels_types.TextInputInteraction;
|
|
101
|
+
confirmPreview(opts: {
|
|
102
|
+
prompt: string;
|
|
103
|
+
previewUrl: string;
|
|
104
|
+
previewLabel?: string;
|
|
105
|
+
approveLabel?: string;
|
|
106
|
+
rejectLabel?: string;
|
|
107
|
+
reasonRequiredOnReject?: boolean;
|
|
108
|
+
}): _vessels_types.ConfirmPreviewInteraction;
|
|
109
|
+
poll(options?: PollOptions): Promise<PollResponse>;
|
|
110
|
+
verifyWebhook(body: string, signature: string): boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHmac } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var VesselsAuthError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "VesselsAuthError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var VesselsValidationError = class extends Error {
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, details) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "VesselsValidationError";
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var VesselsRateLimitError = class extends Error {
|
|
19
|
+
retryAfter;
|
|
20
|
+
constructor(message, retryAfter) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "VesselsRateLimitError";
|
|
23
|
+
this.retryAfter = retryAfter;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var Vessels = class {
|
|
27
|
+
apiKey;
|
|
28
|
+
baseUrl;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.apiKey = config.apiKey;
|
|
31
|
+
this.baseUrl = config.baseUrl?.replace(/\/$/, "") ?? "https://vessels-two.vercel.app";
|
|
32
|
+
}
|
|
33
|
+
async push(payload) {
|
|
34
|
+
const res = await fetch(`${this.baseUrl}/api/v1/push`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(payload)
|
|
41
|
+
});
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
|
|
44
|
+
if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
|
|
45
|
+
if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
|
|
46
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
messageId: data.message_id,
|
|
50
|
+
vesselId: data.vessel_id,
|
|
51
|
+
createdAt: data.created_at
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Interaction helpers
|
|
55
|
+
approval(opts) {
|
|
56
|
+
return { type: "approval", ...opts };
|
|
57
|
+
}
|
|
58
|
+
choice(opts) {
|
|
59
|
+
return { type: "choice", ...opts };
|
|
60
|
+
}
|
|
61
|
+
checklist(opts) {
|
|
62
|
+
return { type: "checklist", ...opts };
|
|
63
|
+
}
|
|
64
|
+
textInput(opts) {
|
|
65
|
+
return { type: "text_input", ...opts };
|
|
66
|
+
}
|
|
67
|
+
confirmPreview(opts) {
|
|
68
|
+
return { type: "confirm_preview", ...opts };
|
|
69
|
+
}
|
|
70
|
+
async poll(options = {}) {
|
|
71
|
+
const { since, limit = 50, ack = true } = options;
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
if (since) params.set("since", since);
|
|
74
|
+
params.set("limit", String(limit));
|
|
75
|
+
params.set("ack", String(ack));
|
|
76
|
+
const res = await fetch(`${this.baseUrl}/api/v1/poll?${params}`, {
|
|
77
|
+
headers: { "Authorization": `Bearer ${this.apiKey}` }
|
|
78
|
+
});
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
|
|
81
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
82
|
+
const events = (data.events ?? []).map((e) => {
|
|
83
|
+
const vessel = {
|
|
84
|
+
id: e.vessel?.id,
|
|
85
|
+
externalId: e.vessel?.external_id ?? null,
|
|
86
|
+
title: e.vessel?.title ?? null,
|
|
87
|
+
metadata: e.vessel?.metadata ?? {}
|
|
88
|
+
};
|
|
89
|
+
if (e.type === "interaction.response") {
|
|
90
|
+
return {
|
|
91
|
+
id: e.id,
|
|
92
|
+
type: "interaction.response",
|
|
93
|
+
timestamp: e.timestamp,
|
|
94
|
+
vessel,
|
|
95
|
+
messageId: e.message_id,
|
|
96
|
+
interactionType: e.interaction_type,
|
|
97
|
+
response: e.response,
|
|
98
|
+
user: e.user ?? null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: e.id,
|
|
103
|
+
type: "message.user",
|
|
104
|
+
timestamp: e.timestamp,
|
|
105
|
+
vessel,
|
|
106
|
+
message: { id: e.message?.id, content: e.message?.content ?? null }
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
return { ok: true, events, hasMore: data.has_more ?? false };
|
|
110
|
+
}
|
|
111
|
+
// Webhook verification — call this in your webhook handler
|
|
112
|
+
// body: raw request body string, signature: X-Vessels-Signature header value
|
|
113
|
+
verifyWebhook(body, signature) {
|
|
114
|
+
if (!signature.startsWith("sha256=")) return false;
|
|
115
|
+
const expected = createHmac("sha256", this.apiKey).update(body).digest("hex");
|
|
116
|
+
const received = signature.slice(7);
|
|
117
|
+
if (expected.length !== received.length) return false;
|
|
118
|
+
let diff = 0;
|
|
119
|
+
for (let i = 0; i < expected.length; i++) {
|
|
120
|
+
diff |= expected.charCodeAt(i) ^ received.charCodeAt(i);
|
|
121
|
+
}
|
|
122
|
+
return diff === 0;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export { Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vessels-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Let your agent reach you. Official Vessels SDK.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["ai", "agents", "vessels", "llm", "notifications"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.5.2",
|
|
27
|
+
"tsup": "^8.5.1",
|
|
28
|
+
"typescript": "^5"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"zod": "^3.22"
|
|
32
|
+
}
|
|
33
|
+
}
|