@builtbyecho/reverbin 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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/client.d.ts +428 -0
- package/dist/client.js +391 -0
- package/dist/webhook-signatures.d.ts +6 -0
- package/dist/webhook-signatures.js +17 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BuiltByEcho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @builtbyecho/reverbin
|
|
2
|
+
|
|
3
|
+
Zero-dependency Node.js ESM SDK for the Reverbin API: agent inboxes, threads, messages, approvals, signed webhooks, API keys, billing, and account lifecycle operations.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install the public Node.js package from npm:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @builtbyecho/reverbin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The SDK requires Node.js 20 or newer, uses Node's built-in `fetch`, and supports ESM imports only. Browser runtimes are not supported; never put a Reverbin API key in client-side code.
|
|
14
|
+
|
|
15
|
+
## Authenticated client
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { ReverbinClient } from '@builtbyecho/reverbin';
|
|
19
|
+
|
|
20
|
+
const apiKey = process.env.REVERBIN_API_KEY;
|
|
21
|
+
const inboxId = process.env.REVERBIN_INBOX_ID;
|
|
22
|
+
if (!apiKey || !inboxId) throw new Error('REVERBIN_API_KEY and REVERBIN_INBOX_ID are required');
|
|
23
|
+
|
|
24
|
+
const reverbin = new ReverbinClient({
|
|
25
|
+
baseUrl: process.env.REVERBIN_BASE_URL ?? 'https://api.reverbin.com',
|
|
26
|
+
apiKey,
|
|
27
|
+
timeoutMs: 30_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const threads = await reverbin.inboxes.threads(inboxId);
|
|
31
|
+
const latest = threads.data[0];
|
|
32
|
+
if (latest) {
|
|
33
|
+
await reverbin.threads.reply(latest.id, {
|
|
34
|
+
text: 'Received — I am handling this from the agent workflow.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Signup already creates the first inbox. Use the returned `REVERBIN_INBOX_ID`; call `reverbin.inboxes.create` only when the workflow needs another inbox and the account has quota.
|
|
40
|
+
|
|
41
|
+
## Self-serve signup
|
|
42
|
+
|
|
43
|
+
The public signup method does not require an API key. It does require a stable caller-owned idempotency key:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { randomUUID } from 'node:crypto';
|
|
47
|
+
import { ReverbinClient } from '@builtbyecho/reverbin';
|
|
48
|
+
|
|
49
|
+
const reverbin = new ReverbinClient();
|
|
50
|
+
const result = await reverbin.signups.create({
|
|
51
|
+
idempotency_key: randomUUID(),
|
|
52
|
+
requester_email: 'builder@example.com',
|
|
53
|
+
agent_name: 'Support Agent',
|
|
54
|
+
agent_use_case: 'Handle customer support replies and escalate unusual requests.',
|
|
55
|
+
preferred_inbox_name: 'support-agent',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (result.credentials_returned) {
|
|
59
|
+
// Store result.api_key.token now; it is returned once.
|
|
60
|
+
console.log(result.inbox.email_address);
|
|
61
|
+
} else {
|
|
62
|
+
// Safe replay: identifiers and a recovery message, but no credentials.
|
|
63
|
+
console.log(result.message);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Keep the original idempotency key with the request outcome. Reuse it only with the identical request body after a lost response.
|
|
68
|
+
|
|
69
|
+
## Core method groups
|
|
70
|
+
|
|
71
|
+
| Group | Methods |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `signups` | `create` |
|
|
74
|
+
| `inboxes` | `create`, `list`, `get`, `threads` |
|
|
75
|
+
| `messages` | `compose`, `list` |
|
|
76
|
+
| `threads` | `get`, `messages`, `reply`, `forward` |
|
|
77
|
+
| `approvals` | `list`, `approve`, `reject` |
|
|
78
|
+
| `webhooks` | `create`, `list`, `deliveries`, `rotateSecret`, `revoke` |
|
|
79
|
+
| `apiKeys` | `create`, `list`, `rotate`, `revoke` |
|
|
80
|
+
| `billing` | `plans`, `checkout`, `portal` |
|
|
81
|
+
| `account` | `export`, `requestDeletion`, `cancelDeletion` |
|
|
82
|
+
| `auditLogs` | `list` |
|
|
83
|
+
| `signupRequests` | `create`, `list`, `update` for legacy operator-assisted workflows |
|
|
84
|
+
|
|
85
|
+
See https://reverbin.com/docs/api for request and response contracts.
|
|
86
|
+
|
|
87
|
+
## Pagination
|
|
88
|
+
|
|
89
|
+
Collection methods return `{ data, next_cursor, has_more }`. Pass `next_cursor` back unchanged as `cursor` to the same method and parent resource:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const first = await reverbin.inboxes.list({ limit: 25 });
|
|
93
|
+
const second = first.has_more
|
|
94
|
+
? await reverbin.inboxes.list({ limit: 25, cursor: first.next_cursor })
|
|
95
|
+
: null;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Cursors are opaque and tenant-, collection-, and parent-bound.
|
|
99
|
+
|
|
100
|
+
## Timeouts, aborts, and errors
|
|
101
|
+
|
|
102
|
+
Requests time out after 30 seconds by default. Configure `timeoutMs` on the client or per request, and pass a caller `AbortSignal` in method request options. The client does not retry requests automatically.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import {
|
|
106
|
+
ReverbinApiError,
|
|
107
|
+
ReverbinClient,
|
|
108
|
+
ReverbinResponseError,
|
|
109
|
+
ReverbinTimeoutError,
|
|
110
|
+
} from '@builtbyecho/reverbin';
|
|
111
|
+
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
try {
|
|
114
|
+
await reverbin.inboxes.list(undefined, {
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
timeoutMs: 5_000,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof ReverbinApiError) {
|
|
120
|
+
console.error(error.status, error.code, error.requestId, error.retryAfterSeconds, error.quota);
|
|
121
|
+
} else if (error instanceof ReverbinTimeoutError) {
|
|
122
|
+
console.error('Timed out after', error.timeoutMs);
|
|
123
|
+
} else if (error instanceof ReverbinResponseError) {
|
|
124
|
+
console.error('Malformed API response', error.status, error.requestId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
API error details are recursively credential-redacted. Even so, do not log one-time signup, API-key, or webhook-secret responses.
|
|
130
|
+
|
|
131
|
+
## Webhook signatures
|
|
132
|
+
|
|
133
|
+
Register only a real reachable HTTPS endpoint, then store the returned secret immediately:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const webhookUrl = process.env.REVERBIN_WEBHOOK_URL;
|
|
137
|
+
if (!webhookUrl) throw new Error('REVERBIN_WEBHOOK_URL is required');
|
|
138
|
+
|
|
139
|
+
const webhook = await reverbin.webhooks.create({
|
|
140
|
+
url: webhookUrl,
|
|
141
|
+
events: ['email.received', 'email.sent', 'email.failed'],
|
|
142
|
+
});
|
|
143
|
+
// Store webhook.secret now; it is returned once.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Verify the signature against the exact raw request bytes before parsing or acting on a webhook:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { verifyWebhookSignature } from '@builtbyecho/reverbin/webhook-signatures';
|
|
150
|
+
|
|
151
|
+
const valid = verifyWebhookSignature(rawBody, signatureHeader, webhookSecret);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
During credential-rotation grace, verify `x-echo-email-signature-previous` separately with the previous secret. The stable `x-echo-email-*` header prefix is retained for wire compatibility.
|
|
155
|
+
|
|
156
|
+
## Retry and idempotency boundaries
|
|
157
|
+
|
|
158
|
+
The SDK never retries automatically.
|
|
159
|
+
|
|
160
|
+
- Signup, Stripe Checkout/Portal, API-key rotation, and webhook-secret rotation require explicit `idempotency_key` inputs. Reuse the same key only with the identical body after a lost response.
|
|
161
|
+
- Do not blindly retry compose, reply, forward, approval decisions, inbox creation, webhook creation/revocation, API-key creation/revocation, or account mutations after an ambiguous response. Inspect resource, thread, audit, or delivery state first.
|
|
162
|
+
- Signup replay prevents duplicate provisioning but intentionally omits one-time credentials.
|
|
163
|
+
|
|
164
|
+
## Launch limitations
|
|
165
|
+
|
|
166
|
+
Outbound attachments are not supported in the launch SDK. Compose, reply, and forward accept text plus optional HTML. Inbound attachments remain available in the authenticated human mail console.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
export declare const REVERBIN_DEFAULT_BASE_URL = "https://api.reverbin.com";
|
|
2
|
+
export declare const REVERBIN_DEFAULT_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare const REVERBIN_MAX_TIMEOUT_MS = 120000;
|
|
4
|
+
export type ReverbinClientOptions = {
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
};
|
|
10
|
+
export type RequestOptions = {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
export type PageOptions = {
|
|
15
|
+
limit?: number;
|
|
16
|
+
cursor?: string;
|
|
17
|
+
};
|
|
18
|
+
export type ListResponse<T> = {
|
|
19
|
+
data: T[];
|
|
20
|
+
next_cursor: string | null;
|
|
21
|
+
has_more: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type DataResponse<T> = {
|
|
24
|
+
data: T[];
|
|
25
|
+
};
|
|
26
|
+
export type ReverbinPolicy = {
|
|
27
|
+
reply_only?: boolean;
|
|
28
|
+
require_approval_for_new_recipients?: boolean;
|
|
29
|
+
require_approval_for_external_domains?: boolean;
|
|
30
|
+
max_outbound_per_hour?: number;
|
|
31
|
+
max_outbound_per_day?: number;
|
|
32
|
+
allowed_domains?: string[];
|
|
33
|
+
blocked_domains?: string[];
|
|
34
|
+
allowed_recipients?: string[];
|
|
35
|
+
blocked_recipients?: string[];
|
|
36
|
+
allow_attachments?: boolean;
|
|
37
|
+
allow_links?: boolean;
|
|
38
|
+
risk_threshold?: 'low' | 'medium' | 'high';
|
|
39
|
+
};
|
|
40
|
+
export type CreateInboxInput = {
|
|
41
|
+
email_address: string;
|
|
42
|
+
display_name?: string;
|
|
43
|
+
agent_id?: string;
|
|
44
|
+
policy?: ReverbinPolicy;
|
|
45
|
+
};
|
|
46
|
+
export type Inbox = {
|
|
47
|
+
id: string;
|
|
48
|
+
email_address: string;
|
|
49
|
+
display_name?: string | null;
|
|
50
|
+
status?: string;
|
|
51
|
+
policy?: ReverbinPolicy;
|
|
52
|
+
created_at?: string;
|
|
53
|
+
updated_at?: string;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
};
|
|
56
|
+
export type MessageDeliveryStatus = 'received' | 'queued' | 'sending' | 'sent' | 'failed' | 'delayed' | 'delivered' | 'bounced' | 'complained';
|
|
57
|
+
export type Message = {
|
|
58
|
+
id: string;
|
|
59
|
+
inbox_id?: string;
|
|
60
|
+
thread_id?: string;
|
|
61
|
+
direction: 'inbound' | 'outbound';
|
|
62
|
+
delivery_status?: MessageDeliveryStatus;
|
|
63
|
+
from_email?: string;
|
|
64
|
+
from_name?: string | null;
|
|
65
|
+
to_json?: string[];
|
|
66
|
+
cc_json?: string[];
|
|
67
|
+
bcc_json?: string[];
|
|
68
|
+
subject: string;
|
|
69
|
+
text_body?: string;
|
|
70
|
+
html_body?: string | null;
|
|
71
|
+
created_at?: string;
|
|
72
|
+
sent_at?: string | null;
|
|
73
|
+
received_at?: string | null;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
export type Thread = {
|
|
77
|
+
id: string;
|
|
78
|
+
inbox_id: string;
|
|
79
|
+
subject: string;
|
|
80
|
+
last_message_at?: string;
|
|
81
|
+
created_at?: string;
|
|
82
|
+
updated_at?: string;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
};
|
|
85
|
+
export type ThreadDetail = Thread & {
|
|
86
|
+
messages: Message[];
|
|
87
|
+
messages_next_cursor: string | null;
|
|
88
|
+
messages_has_more: boolean;
|
|
89
|
+
};
|
|
90
|
+
export type ReplyInput = {
|
|
91
|
+
to?: string[];
|
|
92
|
+
subject?: string;
|
|
93
|
+
text: string;
|
|
94
|
+
html?: string | null;
|
|
95
|
+
};
|
|
96
|
+
export type ComposeInput = {
|
|
97
|
+
inbox_id: string;
|
|
98
|
+
to: string[];
|
|
99
|
+
subject: string;
|
|
100
|
+
text: string;
|
|
101
|
+
html?: string | null;
|
|
102
|
+
};
|
|
103
|
+
export type ForwardInput = {
|
|
104
|
+
to: string[];
|
|
105
|
+
subject: string;
|
|
106
|
+
text: string;
|
|
107
|
+
html?: string | null;
|
|
108
|
+
};
|
|
109
|
+
export type OutboundResult = {
|
|
110
|
+
thread_id?: string;
|
|
111
|
+
message_id?: string;
|
|
112
|
+
send_intent_id?: string;
|
|
113
|
+
approval_id?: string;
|
|
114
|
+
status: 'pending' | 'queued' | 'sent' | 'failed';
|
|
115
|
+
provider_result?: unknown;
|
|
116
|
+
error?: string;
|
|
117
|
+
decision?: string;
|
|
118
|
+
reasons?: string[];
|
|
119
|
+
risk_flags?: string[];
|
|
120
|
+
};
|
|
121
|
+
export type CreateWebhookInput = {
|
|
122
|
+
url: string;
|
|
123
|
+
events?: string[];
|
|
124
|
+
};
|
|
125
|
+
export type WebhookEndpoint = {
|
|
126
|
+
id: string;
|
|
127
|
+
url: string;
|
|
128
|
+
events: string[];
|
|
129
|
+
status: string;
|
|
130
|
+
version: number;
|
|
131
|
+
created_at?: string;
|
|
132
|
+
};
|
|
133
|
+
export type CreatedWebhookEndpoint = WebhookEndpoint & {
|
|
134
|
+
secret: string;
|
|
135
|
+
secret_returned_once: true;
|
|
136
|
+
};
|
|
137
|
+
export type WebhookDelivery = {
|
|
138
|
+
id: string;
|
|
139
|
+
endpoint_id: string;
|
|
140
|
+
generation: number;
|
|
141
|
+
event_type: string;
|
|
142
|
+
payload_json: unknown;
|
|
143
|
+
status: string;
|
|
144
|
+
attempts: number;
|
|
145
|
+
last_error: string | null;
|
|
146
|
+
created_at: string;
|
|
147
|
+
delivered_at: string | null;
|
|
148
|
+
};
|
|
149
|
+
export declare const REVERBIN_API_SCOPES: readonly ["inboxes:read", "inboxes:write", "threads:read", "threads:reply", "webhooks:read", "webhooks:write", "webhook-deliveries:read", "approvals:read", "approvals:write", "billing:read", "billing:write", "audit:read", "account:read", "account:write", "api-keys:read", "api-keys:write"];
|
|
150
|
+
export type ApiScope = typeof REVERBIN_API_SCOPES[number];
|
|
151
|
+
export type ApiKeyMetadata = {
|
|
152
|
+
id: string;
|
|
153
|
+
name: string;
|
|
154
|
+
scopes: ApiScope[];
|
|
155
|
+
version: number;
|
|
156
|
+
created_at: string;
|
|
157
|
+
expires_at: string | null;
|
|
158
|
+
last_used_at: string | null;
|
|
159
|
+
rotated_at: string | null;
|
|
160
|
+
revoked_at: string | null;
|
|
161
|
+
};
|
|
162
|
+
export type CreatedApiKey = ApiKeyMetadata & {
|
|
163
|
+
token: string;
|
|
164
|
+
token_returned_once: true;
|
|
165
|
+
};
|
|
166
|
+
export type CreateApiKeyInput = {
|
|
167
|
+
name: string;
|
|
168
|
+
scopes?: ApiScope[];
|
|
169
|
+
expires_at?: string | null;
|
|
170
|
+
};
|
|
171
|
+
export type CredentialRotationInput = {
|
|
172
|
+
expected_version: number;
|
|
173
|
+
idempotency_key: string;
|
|
174
|
+
};
|
|
175
|
+
export type Approval = {
|
|
176
|
+
id: string;
|
|
177
|
+
inbox_id: string;
|
|
178
|
+
thread_id: string;
|
|
179
|
+
status: 'pending' | 'approved' | 'rejected' | string;
|
|
180
|
+
proposed_to_json?: string[];
|
|
181
|
+
subject?: string;
|
|
182
|
+
body_text?: string;
|
|
183
|
+
body_html?: string | null;
|
|
184
|
+
risk_flags_json?: string[];
|
|
185
|
+
policy_reasons_json?: string[];
|
|
186
|
+
created_at?: string;
|
|
187
|
+
decided_at?: string | null;
|
|
188
|
+
[key: string]: unknown;
|
|
189
|
+
};
|
|
190
|
+
export type AgentSignupInput = {
|
|
191
|
+
idempotency_key: string;
|
|
192
|
+
requester_email: string;
|
|
193
|
+
agent_name: string;
|
|
194
|
+
agent_use_case: string;
|
|
195
|
+
preferred_inbox_name: string;
|
|
196
|
+
webhook_url?: string;
|
|
197
|
+
};
|
|
198
|
+
export type AgentSignupCreatedResult = {
|
|
199
|
+
status: 'provisioned';
|
|
200
|
+
credentials_returned: true;
|
|
201
|
+
signup_request_id: string;
|
|
202
|
+
tenant_id: string;
|
|
203
|
+
agent: {
|
|
204
|
+
id: string;
|
|
205
|
+
name: string;
|
|
206
|
+
};
|
|
207
|
+
inbox: Inbox;
|
|
208
|
+
api_key: {
|
|
209
|
+
id: string;
|
|
210
|
+
token: string;
|
|
211
|
+
scopes: ApiScope[];
|
|
212
|
+
version: number;
|
|
213
|
+
returned_once: true;
|
|
214
|
+
};
|
|
215
|
+
webhook?: CreatedWebhookEndpoint | null;
|
|
216
|
+
quickstart: {
|
|
217
|
+
base_url: string;
|
|
218
|
+
inbox_email: string;
|
|
219
|
+
next_steps: string[];
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
export type AgentSignupReplayResult = {
|
|
223
|
+
status: 'provisioned';
|
|
224
|
+
credentials_returned: false;
|
|
225
|
+
signup_request_id: string;
|
|
226
|
+
tenant_id: string;
|
|
227
|
+
agent: {
|
|
228
|
+
id: string;
|
|
229
|
+
name: string;
|
|
230
|
+
};
|
|
231
|
+
inbox: Inbox;
|
|
232
|
+
message: string;
|
|
233
|
+
};
|
|
234
|
+
export type AgentSignupResult = AgentSignupCreatedResult | AgentSignupReplayResult;
|
|
235
|
+
export type SignupVerificationCheck = {
|
|
236
|
+
key: string;
|
|
237
|
+
label: string;
|
|
238
|
+
required: boolean;
|
|
239
|
+
status: 'pending' | 'passed' | 'failed';
|
|
240
|
+
evidence?: string;
|
|
241
|
+
};
|
|
242
|
+
export type SignupRequest = {
|
|
243
|
+
id: string;
|
|
244
|
+
requester_email: string;
|
|
245
|
+
requester_name?: string | null;
|
|
246
|
+
agent_use_case: string;
|
|
247
|
+
preferred_inbox_name?: string | null;
|
|
248
|
+
webhook_url?: string | null;
|
|
249
|
+
status: string;
|
|
250
|
+
verification_json?: SignupVerificationCheck[];
|
|
251
|
+
notes?: string | null;
|
|
252
|
+
verification_summary?: unknown;
|
|
253
|
+
created_at?: string;
|
|
254
|
+
updated_at?: string;
|
|
255
|
+
[key: string]: unknown;
|
|
256
|
+
};
|
|
257
|
+
export type CreateSignupRequestInput = {
|
|
258
|
+
requester_email: string;
|
|
259
|
+
requester_name?: string;
|
|
260
|
+
agent_use_case: string;
|
|
261
|
+
preferred_inbox_name?: string;
|
|
262
|
+
webhook_url?: string;
|
|
263
|
+
notes?: string;
|
|
264
|
+
};
|
|
265
|
+
export type UpdateSignupRequestInput = {
|
|
266
|
+
status?: 'pending' | 'approved' | 'rejected' | 'provisioned';
|
|
267
|
+
verification?: SignupVerificationCheck[];
|
|
268
|
+
notes?: string;
|
|
269
|
+
};
|
|
270
|
+
export type BillingPlanKey = 'free' | 'developer' | 'startup' | 'enterprise';
|
|
271
|
+
export type BillingPlan = {
|
|
272
|
+
key: BillingPlanKey;
|
|
273
|
+
label: string;
|
|
274
|
+
price_monthly_usd: number | null;
|
|
275
|
+
max_inboxes: number;
|
|
276
|
+
emails_per_month: number | null;
|
|
277
|
+
max_webhooks: number;
|
|
278
|
+
};
|
|
279
|
+
export type CreateCheckoutInput = {
|
|
280
|
+
plan: 'developer' | 'startup';
|
|
281
|
+
idempotency_key: string;
|
|
282
|
+
success_url?: string;
|
|
283
|
+
cancel_url?: string;
|
|
284
|
+
};
|
|
285
|
+
export type BillingSession = {
|
|
286
|
+
id: string;
|
|
287
|
+
url: string | null;
|
|
288
|
+
provider: string;
|
|
289
|
+
plan?: 'developer' | 'startup';
|
|
290
|
+
link_enabled_by_stripe?: boolean;
|
|
291
|
+
};
|
|
292
|
+
export type CreateBillingPortalInput = {
|
|
293
|
+
idempotency_key: string;
|
|
294
|
+
return_url?: string;
|
|
295
|
+
};
|
|
296
|
+
export type AuditLog = {
|
|
297
|
+
id: string;
|
|
298
|
+
action: string;
|
|
299
|
+
target_type?: string;
|
|
300
|
+
target_id?: string;
|
|
301
|
+
metadata_json?: unknown;
|
|
302
|
+
created_at: string;
|
|
303
|
+
[key: string]: unknown;
|
|
304
|
+
};
|
|
305
|
+
export type AccountExport = {
|
|
306
|
+
exported_at?: string;
|
|
307
|
+
limit_per_collection?: number;
|
|
308
|
+
tenant?: Record<string, unknown>;
|
|
309
|
+
[collection: string]: unknown;
|
|
310
|
+
};
|
|
311
|
+
export type AccountDeletionPending = {
|
|
312
|
+
id: string;
|
|
313
|
+
account_status: 'deletion_pending';
|
|
314
|
+
deletion_requested_at: string;
|
|
315
|
+
deletion_due_at: string;
|
|
316
|
+
};
|
|
317
|
+
export type AccountDeletionCancelled = {
|
|
318
|
+
id: string;
|
|
319
|
+
account_status: 'active';
|
|
320
|
+
deletion_requested_at: null;
|
|
321
|
+
deletion_due_at: null;
|
|
322
|
+
};
|
|
323
|
+
export type QuotaMetadata = {
|
|
324
|
+
limit: number;
|
|
325
|
+
used: number;
|
|
326
|
+
resetAt: string | null;
|
|
327
|
+
};
|
|
328
|
+
type ErrorMetadata = {
|
|
329
|
+
requestId?: string;
|
|
330
|
+
retryAfterSeconds?: number;
|
|
331
|
+
apiKey?: string;
|
|
332
|
+
};
|
|
333
|
+
export declare class ReverbinApiError extends Error {
|
|
334
|
+
readonly status: number;
|
|
335
|
+
readonly code: string;
|
|
336
|
+
readonly requestId?: string;
|
|
337
|
+
readonly retryAfterSeconds?: number;
|
|
338
|
+
readonly quota?: QuotaMetadata;
|
|
339
|
+
readonly details: unknown;
|
|
340
|
+
constructor(status: number, body: unknown, metadata?: ErrorMetadata);
|
|
341
|
+
/** @deprecated Prefer `details`; this alias remains for pre-1.0 compatibility. */
|
|
342
|
+
get body(): unknown;
|
|
343
|
+
toJSON(): {
|
|
344
|
+
name: string;
|
|
345
|
+
message: string;
|
|
346
|
+
status: number;
|
|
347
|
+
code: string;
|
|
348
|
+
requestId: string | undefined;
|
|
349
|
+
retryAfterSeconds: number | undefined;
|
|
350
|
+
quota: QuotaMetadata | undefined;
|
|
351
|
+
details: unknown;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
export declare class ReverbinResponseError extends Error {
|
|
355
|
+
readonly status: number;
|
|
356
|
+
readonly code = "invalid_json_response";
|
|
357
|
+
readonly requestId?: string;
|
|
358
|
+
constructor(status: number, requestId?: string);
|
|
359
|
+
}
|
|
360
|
+
export declare class ReverbinTimeoutError extends Error {
|
|
361
|
+
readonly timeoutMs: number;
|
|
362
|
+
constructor(timeoutMs: number);
|
|
363
|
+
}
|
|
364
|
+
export declare class ReverbinClient {
|
|
365
|
+
#private;
|
|
366
|
+
readonly baseUrl: string;
|
|
367
|
+
constructor(options?: ReverbinClientOptions);
|
|
368
|
+
readonly signups: {
|
|
369
|
+
create: (input: AgentSignupInput, options?: RequestOptions) => Promise<AgentSignupResult>;
|
|
370
|
+
};
|
|
371
|
+
readonly inboxes: {
|
|
372
|
+
create: (input: CreateInboxInput, options?: RequestOptions) => Promise<Inbox>;
|
|
373
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<Inbox>>;
|
|
374
|
+
get: (id: string, options?: RequestOptions) => Promise<Inbox>;
|
|
375
|
+
threads: (id: string, page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<Thread>>;
|
|
376
|
+
};
|
|
377
|
+
readonly messages: {
|
|
378
|
+
compose: (input: ComposeInput, options?: RequestOptions) => Promise<OutboundResult>;
|
|
379
|
+
list: (threadId: string, page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<Message>>;
|
|
380
|
+
};
|
|
381
|
+
readonly threads: {
|
|
382
|
+
get: (id: string, page?: PageOptions, options?: RequestOptions) => Promise<ThreadDetail>;
|
|
383
|
+
messages: (id: string, page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<Message>>;
|
|
384
|
+
reply: (id: string, input: ReplyInput, options?: RequestOptions) => Promise<OutboundResult>;
|
|
385
|
+
forward: (id: string, input: ForwardInput, options?: RequestOptions) => Promise<OutboundResult>;
|
|
386
|
+
};
|
|
387
|
+
readonly approvals: {
|
|
388
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<Approval>>;
|
|
389
|
+
approve: (id: string, options?: RequestOptions) => Promise<OutboundResult>;
|
|
390
|
+
reject: (id: string, options?: RequestOptions) => Promise<Approval>;
|
|
391
|
+
};
|
|
392
|
+
readonly webhooks: {
|
|
393
|
+
create: (input: CreateWebhookInput, options?: RequestOptions) => Promise<CreatedWebhookEndpoint>;
|
|
394
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<WebhookEndpoint>>;
|
|
395
|
+
deliveries: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<WebhookDelivery>>;
|
|
396
|
+
rotateSecret: (id: string, input: CredentialRotationInput, options?: RequestOptions) => Promise<CreatedWebhookEndpoint>;
|
|
397
|
+
revoke: (id: string, options?: RequestOptions) => Promise<void>;
|
|
398
|
+
};
|
|
399
|
+
readonly apiKeys: {
|
|
400
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<ApiKeyMetadata>>;
|
|
401
|
+
create: (input: CreateApiKeyInput, options?: RequestOptions) => Promise<CreatedApiKey>;
|
|
402
|
+
rotate: (id: string, input: CredentialRotationInput, options?: RequestOptions) => Promise<CreatedApiKey>;
|
|
403
|
+
revoke: (id: string, options?: RequestOptions) => Promise<{
|
|
404
|
+
id: string;
|
|
405
|
+
status: "revoked";
|
|
406
|
+
}>;
|
|
407
|
+
};
|
|
408
|
+
readonly account: {
|
|
409
|
+
export: (options?: RequestOptions) => Promise<AccountExport>;
|
|
410
|
+
requestDeletion: (confirmation: "DELETE", options?: RequestOptions) => Promise<AccountDeletionPending>;
|
|
411
|
+
cancelDeletion: (options?: RequestOptions) => Promise<AccountDeletionCancelled>;
|
|
412
|
+
};
|
|
413
|
+
readonly billing: {
|
|
414
|
+
plans: (options?: RequestOptions) => Promise<DataResponse<BillingPlan>>;
|
|
415
|
+
checkout: (input: CreateCheckoutInput, options?: RequestOptions) => Promise<BillingSession>;
|
|
416
|
+
portal: (input: CreateBillingPortalInput, options?: RequestOptions) => Promise<BillingSession>;
|
|
417
|
+
};
|
|
418
|
+
readonly auditLogs: {
|
|
419
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<AuditLog>>;
|
|
420
|
+
};
|
|
421
|
+
readonly signupRequests: {
|
|
422
|
+
create: (input: CreateSignupRequestInput, options?: RequestOptions) => Promise<SignupRequest>;
|
|
423
|
+
list: (page?: PageOptions, options?: RequestOptions) => Promise<ListResponse<SignupRequest>>;
|
|
424
|
+
update: (id: string, input: UpdateSignupRequestInput, options?: RequestOptions) => Promise<SignupRequest>;
|
|
425
|
+
};
|
|
426
|
+
private request;
|
|
427
|
+
}
|
|
428
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
export const REVERBIN_DEFAULT_BASE_URL = 'https://api.reverbin.com';
|
|
2
|
+
export const REVERBIN_DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
export const REVERBIN_MAX_TIMEOUT_MS = 120_000;
|
|
4
|
+
export const REVERBIN_API_SCOPES = [
|
|
5
|
+
'inboxes:read',
|
|
6
|
+
'inboxes:write',
|
|
7
|
+
'threads:read',
|
|
8
|
+
'threads:reply',
|
|
9
|
+
'webhooks:read',
|
|
10
|
+
'webhooks:write',
|
|
11
|
+
'webhook-deliveries:read',
|
|
12
|
+
'approvals:read',
|
|
13
|
+
'approvals:write',
|
|
14
|
+
'billing:read',
|
|
15
|
+
'billing:write',
|
|
16
|
+
'audit:read',
|
|
17
|
+
'account:read',
|
|
18
|
+
'account:write',
|
|
19
|
+
'api-keys:read',
|
|
20
|
+
'api-keys:write',
|
|
21
|
+
];
|
|
22
|
+
const REDACTED = '[REDACTED]';
|
|
23
|
+
const SENSITIVE_KEY = /authorization|api[_-]?key|token|secret|password|credential/i;
|
|
24
|
+
const REVERBIN_TOKEN = /\brvb_[A-Za-z0-9_-]{8,}\b/g;
|
|
25
|
+
const REVERBIN_TOKEN_PATTERN = /\brvb_[A-Za-z0-9_-]{8,}\b/;
|
|
26
|
+
function sanitizeErrorString(value, apiKey) {
|
|
27
|
+
const withoutKnownKey = apiKey ? value.split(apiKey).join(REDACTED) : value;
|
|
28
|
+
return withoutKnownKey.replace(REVERBIN_TOKEN, REDACTED);
|
|
29
|
+
}
|
|
30
|
+
function sanitizeErrorValue(value, apiKey, depth = 0) {
|
|
31
|
+
if (depth > 8)
|
|
32
|
+
return '[TRUNCATED]';
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return sanitizeErrorString(value, apiKey);
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map((item) => sanitizeErrorValue(item, apiKey, depth + 1));
|
|
37
|
+
if (!value || typeof value !== 'object')
|
|
38
|
+
return value;
|
|
39
|
+
const output = {};
|
|
40
|
+
for (const [key, child] of Object.entries(value)) {
|
|
41
|
+
output[key] = SENSITIVE_KEY.test(key) ? REDACTED : sanitizeErrorValue(child, apiKey, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
function errorRecord(body) {
|
|
46
|
+
return body && typeof body === 'object' && !Array.isArray(body) ? body : undefined;
|
|
47
|
+
}
|
|
48
|
+
function safeErrorCode(body, apiKey) {
|
|
49
|
+
const record = errorRecord(body);
|
|
50
|
+
const candidate = record?.error ?? record?.code;
|
|
51
|
+
return typeof candidate === 'string'
|
|
52
|
+
&& (!apiKey || !candidate.includes(apiKey))
|
|
53
|
+
&& !REVERBIN_TOKEN_PATTERN.test(candidate)
|
|
54
|
+
&& /^[a-zA-Z0-9_.:-]{1,120}$/.test(candidate)
|
|
55
|
+
? candidate
|
|
56
|
+
: 'http_error';
|
|
57
|
+
}
|
|
58
|
+
function quotaMetadata(body) {
|
|
59
|
+
const record = errorRecord(body);
|
|
60
|
+
if (!record || !Number.isFinite(record.limit) || !Number.isFinite(record.used))
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
limit: Number(record.limit),
|
|
64
|
+
used: Number(record.used),
|
|
65
|
+
resetAt: typeof record.reset_at === 'string' && Number.isFinite(Date.parse(record.reset_at)) ? record.reset_at : null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export class ReverbinApiError extends Error {
|
|
69
|
+
status;
|
|
70
|
+
code;
|
|
71
|
+
requestId;
|
|
72
|
+
retryAfterSeconds;
|
|
73
|
+
quota;
|
|
74
|
+
details;
|
|
75
|
+
constructor(status, body, metadata = {}) {
|
|
76
|
+
const code = safeErrorCode(body, metadata.apiKey);
|
|
77
|
+
super(`Reverbin API request failed (${status} ${code})`);
|
|
78
|
+
this.name = 'ReverbinApiError';
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.requestId = metadata.requestId ? sanitizeErrorString(metadata.requestId, metadata.apiKey) : undefined;
|
|
82
|
+
this.retryAfterSeconds = metadata.retryAfterSeconds;
|
|
83
|
+
this.quota = quotaMetadata(body);
|
|
84
|
+
this.details = sanitizeErrorValue(body, metadata.apiKey);
|
|
85
|
+
}
|
|
86
|
+
/** @deprecated Prefer `details`; this alias remains for pre-1.0 compatibility. */
|
|
87
|
+
get body() {
|
|
88
|
+
return this.details;
|
|
89
|
+
}
|
|
90
|
+
toJSON() {
|
|
91
|
+
return {
|
|
92
|
+
name: this.name,
|
|
93
|
+
message: this.message,
|
|
94
|
+
status: this.status,
|
|
95
|
+
code: this.code,
|
|
96
|
+
requestId: this.requestId,
|
|
97
|
+
retryAfterSeconds: this.retryAfterSeconds,
|
|
98
|
+
quota: this.quota,
|
|
99
|
+
details: this.details,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export class ReverbinResponseError extends Error {
|
|
104
|
+
status;
|
|
105
|
+
code = 'invalid_json_response';
|
|
106
|
+
requestId;
|
|
107
|
+
constructor(status, requestId) {
|
|
108
|
+
super(`Reverbin API returned malformed JSON (status ${status})`);
|
|
109
|
+
this.name = 'ReverbinResponseError';
|
|
110
|
+
this.status = status;
|
|
111
|
+
this.requestId = requestId;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class ReverbinTimeoutError extends Error {
|
|
115
|
+
timeoutMs;
|
|
116
|
+
constructor(timeoutMs) {
|
|
117
|
+
super(`Reverbin API request timed out after ${timeoutMs}ms`);
|
|
118
|
+
this.name = 'ReverbinTimeoutError';
|
|
119
|
+
this.timeoutMs = timeoutMs;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validateTimeout(value, fallback) {
|
|
123
|
+
const timeoutMs = value ?? fallback;
|
|
124
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > REVERBIN_MAX_TIMEOUT_MS) {
|
|
125
|
+
throw new TypeError(`timeoutMs must be an integer from 1 through ${REVERBIN_MAX_TIMEOUT_MS}`);
|
|
126
|
+
}
|
|
127
|
+
return timeoutMs;
|
|
128
|
+
}
|
|
129
|
+
function normalizeBaseUrl(value) {
|
|
130
|
+
let url;
|
|
131
|
+
try {
|
|
132
|
+
url = new URL(value);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new TypeError('baseUrl must be an absolute HTTP or HTTPS URL');
|
|
136
|
+
}
|
|
137
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
138
|
+
throw new TypeError('baseUrl must use HTTP or HTTPS');
|
|
139
|
+
}
|
|
140
|
+
if (url.username || url.password)
|
|
141
|
+
throw new TypeError('baseUrl must not contain credentials');
|
|
142
|
+
if (url.search || url.hash)
|
|
143
|
+
throw new TypeError('baseUrl must not contain a query or fragment');
|
|
144
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/`;
|
|
145
|
+
return url;
|
|
146
|
+
}
|
|
147
|
+
function normalizeApiKey(value) {
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
if (!value || value.length > 4_096 || value !== value.trim() || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
151
|
+
throw new TypeError('apiKey must be a non-empty value without surrounding whitespace');
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function encodedId(value, label) {
|
|
156
|
+
if (typeof value !== 'string' || !value.trim() || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
157
|
+
throw new TypeError(`${label} must be a non-empty identifier`);
|
|
158
|
+
}
|
|
159
|
+
return encodeURIComponent(value);
|
|
160
|
+
}
|
|
161
|
+
function idempotencyKey(value) {
|
|
162
|
+
if (typeof value !== 'string' || value !== value.trim() || value.length < 1 || value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
163
|
+
throw new TypeError('idempotency_key must contain 1 to 200 non-control characters');
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
function pagePath(path, options) {
|
|
168
|
+
if (!options)
|
|
169
|
+
return path;
|
|
170
|
+
const params = new URLSearchParams();
|
|
171
|
+
if (options.limit !== undefined) {
|
|
172
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100) {
|
|
173
|
+
throw new TypeError('limit must be an integer from 1 through 100');
|
|
174
|
+
}
|
|
175
|
+
params.set('limit', String(options.limit));
|
|
176
|
+
}
|
|
177
|
+
if (options.cursor !== undefined) {
|
|
178
|
+
if (typeof options.cursor !== 'string' || !options.cursor || options.cursor.length > 2048) {
|
|
179
|
+
throw new TypeError('cursor must be a non-empty opaque string no longer than 2048 characters');
|
|
180
|
+
}
|
|
181
|
+
params.set('cursor', options.cursor);
|
|
182
|
+
}
|
|
183
|
+
const query = params.toString();
|
|
184
|
+
return query ? `${path}?${query}` : path;
|
|
185
|
+
}
|
|
186
|
+
function retryAfterSeconds(response, body) {
|
|
187
|
+
const value = response.headers.get('retry-after');
|
|
188
|
+
if (value) {
|
|
189
|
+
const seconds = Number(value);
|
|
190
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
191
|
+
return Math.ceil(seconds);
|
|
192
|
+
const at = Date.parse(value);
|
|
193
|
+
if (Number.isFinite(at))
|
|
194
|
+
return Math.max(0, Math.ceil((at - Date.now()) / 1_000));
|
|
195
|
+
}
|
|
196
|
+
const bodyValue = errorRecord(body)?.retry_after_seconds;
|
|
197
|
+
return Number.isFinite(bodyValue) && Number(bodyValue) >= 0 ? Math.ceil(Number(bodyValue)) : undefined;
|
|
198
|
+
}
|
|
199
|
+
function requestId(response, body, apiKey) {
|
|
200
|
+
const header = response.headers.get('x-request-id') ?? response.headers.get('request-id');
|
|
201
|
+
if (header)
|
|
202
|
+
return sanitizeErrorString(header.slice(0, 200), apiKey);
|
|
203
|
+
const value = errorRecord(body)?.request_id;
|
|
204
|
+
return typeof value === 'string' ? sanitizeErrorString(value.slice(0, 200), apiKey) : undefined;
|
|
205
|
+
}
|
|
206
|
+
function composedSignal(callerSignal, timeoutMs) {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
let timedOut = false;
|
|
209
|
+
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
|
210
|
+
if (callerSignal?.aborted)
|
|
211
|
+
abortFromCaller();
|
|
212
|
+
else
|
|
213
|
+
callerSignal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
timedOut = true;
|
|
216
|
+
controller.abort();
|
|
217
|
+
}, timeoutMs);
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
return {
|
|
220
|
+
signal: controller.signal,
|
|
221
|
+
didTimeOut: () => timedOut,
|
|
222
|
+
cleanup: () => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
callerSignal?.removeEventListener('abort', abortFromCaller);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function parseResponseBody(response, responseRequestId) {
|
|
229
|
+
if (response.status === 204 || response.status === 205)
|
|
230
|
+
return undefined;
|
|
231
|
+
const text = await response.text();
|
|
232
|
+
if (!text)
|
|
233
|
+
return null;
|
|
234
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
235
|
+
if (contentType.includes('application/json') || contentType.includes('+json')) {
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(text);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
throw new ReverbinResponseError(response.status, responseRequestId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (response.ok)
|
|
244
|
+
throw new ReverbinResponseError(response.status, responseRequestId);
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
export class ReverbinClient {
|
|
248
|
+
baseUrl;
|
|
249
|
+
#baseUrl;
|
|
250
|
+
#apiKey;
|
|
251
|
+
#fetchImpl;
|
|
252
|
+
#timeoutMs;
|
|
253
|
+
constructor(options = {}) {
|
|
254
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? REVERBIN_DEFAULT_BASE_URL);
|
|
255
|
+
this.baseUrl = this.#baseUrl.href.replace(/\/$/, '');
|
|
256
|
+
this.#apiKey = normalizeApiKey(options.apiKey);
|
|
257
|
+
this.#timeoutMs = validateTimeout(options.timeoutMs, REVERBIN_DEFAULT_TIMEOUT_MS);
|
|
258
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
259
|
+
if (typeof fetchImpl !== 'function')
|
|
260
|
+
throw new TypeError('fetch is not available; pass ReverbinClient({ fetch })');
|
|
261
|
+
this.#fetchImpl = fetchImpl;
|
|
262
|
+
}
|
|
263
|
+
signups = {
|
|
264
|
+
create: (input, options) => this.request('/v1/agent-signups', {
|
|
265
|
+
...options,
|
|
266
|
+
method: 'POST',
|
|
267
|
+
body: {
|
|
268
|
+
requester_email: input.requester_email,
|
|
269
|
+
agent_name: input.agent_name,
|
|
270
|
+
agent_use_case: input.agent_use_case,
|
|
271
|
+
preferred_inbox_name: input.preferred_inbox_name,
|
|
272
|
+
...(input.webhook_url === undefined ? {} : { webhook_url: input.webhook_url }),
|
|
273
|
+
},
|
|
274
|
+
idempotencyKey: idempotencyKey(input.idempotency_key),
|
|
275
|
+
}),
|
|
276
|
+
};
|
|
277
|
+
inboxes = {
|
|
278
|
+
create: (input, options) => this.request('/v1/inboxes', { ...options, method: 'POST', body: input }),
|
|
279
|
+
list: (page, options) => this.request(pagePath('/v1/inboxes', page), options),
|
|
280
|
+
get: (id, options) => this.request(`/v1/inboxes/${encodedId(id, 'inbox id')}`, options),
|
|
281
|
+
threads: (id, page, options) => this.request(pagePath(`/v1/inboxes/${encodedId(id, 'inbox id')}/threads`, page), options),
|
|
282
|
+
};
|
|
283
|
+
messages = {
|
|
284
|
+
compose: (input, options) => this.request('/v1/messages', { ...options, method: 'POST', body: input }),
|
|
285
|
+
list: (threadId, page, options) => this.request(pagePath(`/v1/threads/${encodedId(threadId, 'thread id')}/messages`, page), options),
|
|
286
|
+
};
|
|
287
|
+
threads = {
|
|
288
|
+
get: (id, page, options) => this.request(pagePath(`/v1/threads/${encodedId(id, 'thread id')}`, page), options),
|
|
289
|
+
messages: (id, page, options) => this.request(pagePath(`/v1/threads/${encodedId(id, 'thread id')}/messages`, page), options),
|
|
290
|
+
reply: (id, input, options) => this.request(`/v1/threads/${encodedId(id, 'thread id')}/reply`, { ...options, method: 'POST', body: input }),
|
|
291
|
+
forward: (id, input, options) => this.request(`/v1/threads/${encodedId(id, 'thread id')}/forward`, { ...options, method: 'POST', body: input }),
|
|
292
|
+
};
|
|
293
|
+
approvals = {
|
|
294
|
+
list: (page, options) => this.request(pagePath('/v1/approvals', page), options),
|
|
295
|
+
approve: (id, options) => this.request(`/v1/approvals/${encodedId(id, 'approval id')}/approve`, { ...options, method: 'POST' }),
|
|
296
|
+
reject: (id, options) => this.request(`/v1/approvals/${encodedId(id, 'approval id')}/reject`, { ...options, method: 'POST' }),
|
|
297
|
+
};
|
|
298
|
+
webhooks = {
|
|
299
|
+
create: (input, options) => this.request('/v1/webhooks', { ...options, method: 'POST', body: input }),
|
|
300
|
+
list: (page, options) => this.request(pagePath('/v1/webhooks', page), options),
|
|
301
|
+
deliveries: (page, options) => this.request(pagePath('/v1/webhook-deliveries', page), options),
|
|
302
|
+
rotateSecret: (id, input, options) => this.request(`/v1/webhooks/${encodedId(id, 'webhook id')}/rotate-secret`, {
|
|
303
|
+
...options,
|
|
304
|
+
method: 'POST',
|
|
305
|
+
body: { expected_version: input.expected_version },
|
|
306
|
+
idempotencyKey: idempotencyKey(input.idempotency_key),
|
|
307
|
+
}),
|
|
308
|
+
revoke: (id, options) => this.request(`/v1/webhooks/${encodedId(id, 'webhook id')}`, { ...options, method: 'DELETE' }),
|
|
309
|
+
};
|
|
310
|
+
apiKeys = {
|
|
311
|
+
list: (page, options) => this.request(pagePath('/v1/api-keys', page), options),
|
|
312
|
+
create: (input, options) => this.request('/v1/api-keys', { ...options, method: 'POST', body: input }),
|
|
313
|
+
rotate: (id, input, options) => this.request(`/v1/api-keys/${encodedId(id, 'API key id')}/rotate`, {
|
|
314
|
+
...options,
|
|
315
|
+
method: 'POST',
|
|
316
|
+
body: { expected_version: input.expected_version },
|
|
317
|
+
idempotencyKey: idempotencyKey(input.idempotency_key),
|
|
318
|
+
}),
|
|
319
|
+
revoke: (id, options) => this.request(`/v1/api-keys/${encodedId(id, 'API key id')}/revoke`, { ...options, method: 'POST' }),
|
|
320
|
+
};
|
|
321
|
+
account = {
|
|
322
|
+
export: (options) => this.request('/v1/account/export', options),
|
|
323
|
+
requestDeletion: (confirmation, options) => this.request('/v1/account/deletion-request', { ...options, method: 'POST', body: { confirmation } }),
|
|
324
|
+
cancelDeletion: (options) => this.request('/v1/account/deletion-request/cancel', { ...options, method: 'POST' }),
|
|
325
|
+
};
|
|
326
|
+
billing = {
|
|
327
|
+
plans: (options) => this.request('/v1/billing/plans', options),
|
|
328
|
+
checkout: (input, options) => this.request('/v1/billing/checkout', {
|
|
329
|
+
...options,
|
|
330
|
+
method: 'POST',
|
|
331
|
+
body: {
|
|
332
|
+
plan: input.plan,
|
|
333
|
+
...(input.success_url === undefined ? {} : { success_url: input.success_url }),
|
|
334
|
+
...(input.cancel_url === undefined ? {} : { cancel_url: input.cancel_url }),
|
|
335
|
+
},
|
|
336
|
+
idempotencyKey: idempotencyKey(input.idempotency_key),
|
|
337
|
+
}),
|
|
338
|
+
portal: (input, options) => this.request('/v1/billing/portal', {
|
|
339
|
+
...options,
|
|
340
|
+
method: 'POST',
|
|
341
|
+
body: input.return_url === undefined ? {} : { return_url: input.return_url },
|
|
342
|
+
idempotencyKey: idempotencyKey(input.idempotency_key),
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
auditLogs = {
|
|
346
|
+
list: (page, options) => this.request(pagePath('/v1/audit-logs', page), options),
|
|
347
|
+
};
|
|
348
|
+
signupRequests = {
|
|
349
|
+
create: (input, options) => this.request('/v1/signup-requests', { ...options, method: 'POST', body: input }),
|
|
350
|
+
list: (page, options) => this.request(pagePath('/v1/signup-requests', page), options),
|
|
351
|
+
update: (id, input, options) => this.request(`/v1/signup-requests/${encodedId(id, 'signup request id')}`, { ...options, method: 'PATCH', body: input }),
|
|
352
|
+
};
|
|
353
|
+
async request(path, options = {}) {
|
|
354
|
+
const timeoutMs = validateTimeout(options.timeoutMs, this.#timeoutMs);
|
|
355
|
+
const composed = composedSignal(options.signal, timeoutMs);
|
|
356
|
+
const headers = { accept: 'application/json' };
|
|
357
|
+
if (this.#apiKey)
|
|
358
|
+
headers.authorization = `Bearer ${this.#apiKey}`;
|
|
359
|
+
if (options.idempotencyKey)
|
|
360
|
+
headers['idempotency-key'] = options.idempotencyKey;
|
|
361
|
+
if (options.body !== undefined)
|
|
362
|
+
headers['content-type'] = 'application/json';
|
|
363
|
+
const url = new URL(path.replace(/^\/+/, ''), this.#baseUrl);
|
|
364
|
+
try {
|
|
365
|
+
const response = await this.#fetchImpl(url, {
|
|
366
|
+
method: options.method ?? 'GET',
|
|
367
|
+
headers,
|
|
368
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
369
|
+
signal: composed.signal,
|
|
370
|
+
});
|
|
371
|
+
const headerRequestId = requestId(response, undefined, this.#apiKey);
|
|
372
|
+
const body = await parseResponseBody(response, headerRequestId);
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
throw new ReverbinApiError(response.status, body, {
|
|
375
|
+
requestId: requestId(response, body, this.#apiKey),
|
|
376
|
+
retryAfterSeconds: retryAfterSeconds(response, body),
|
|
377
|
+
apiKey: this.#apiKey,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return body;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (composed.didTimeOut())
|
|
384
|
+
throw new ReverbinTimeoutError(timeoutMs);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
composed.cleanup();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type WebhookPayload = string | Uint8Array;
|
|
2
|
+
/**
|
|
3
|
+
* Verify Reverbin's `sha256=<hex>` signature over the exact raw request body.
|
|
4
|
+
* Call this once per candidate secret/header during a webhook-secret rotation grace period.
|
|
5
|
+
*/
|
|
6
|
+
export declare function verifyWebhookSignature(rawBody: WebhookPayload, signature: string | null | undefined, secret: string): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Verify Reverbin's `sha256=<hex>` signature over the exact raw request body.
|
|
4
|
+
* Call this once per candidate secret/header during a webhook-secret rotation grace period.
|
|
5
|
+
*/
|
|
6
|
+
export function verifyWebhookSignature(rawBody, signature, secret) {
|
|
7
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
8
|
+
throw new TypeError('webhook secret must be a non-empty string');
|
|
9
|
+
}
|
|
10
|
+
const expected = createHmac('sha256', secret).update(rawBody).digest();
|
|
11
|
+
const validFormat = typeof signature === 'string' && /^sha256=[0-9a-f]{64}$/.test(signature);
|
|
12
|
+
const supplied = validFormat
|
|
13
|
+
? Buffer.from(signature.slice('sha256='.length), 'hex')
|
|
14
|
+
: Buffer.alloc(expected.length);
|
|
15
|
+
const equal = timingSafeEqual(expected, supplied);
|
|
16
|
+
return validFormat && equal;
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@builtbyecho/reverbin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js ESM SDK for Reverbin agent inboxes, threads, messages, approvals, webhooks, credentials, billing, and account lifecycle APIs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/client.js",
|
|
7
|
+
"types": "./dist/client.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/client.d.ts",
|
|
11
|
+
"import": "./dist/client.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./dist/client.d.ts",
|
|
15
|
+
"import": "./dist/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./webhook-signatures": {
|
|
18
|
+
"types": "./dist/webhook-signatures.d.ts",
|
|
19
|
+
"import": "./dist/webhook-signatures.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Wdustin1/reverbin.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Wdustin1/reverbin/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://reverbin.com",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"agents",
|
|
46
|
+
"email",
|
|
47
|
+
"inbox",
|
|
48
|
+
"reverbin",
|
|
49
|
+
"sdk",
|
|
50
|
+
"typescript",
|
|
51
|
+
"webhooks"
|
|
52
|
+
]
|
|
53
|
+
}
|