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 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;
@@ -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 };
@@ -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
+ }