@vaultsandbox/client 0.5.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +612 -0
  3. package/dist/client.d.ts +231 -0
  4. package/dist/client.js +432 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/crypto/constants.d.ts +13 -0
  7. package/dist/crypto/constants.js +14 -0
  8. package/dist/crypto/constants.js.map +1 -0
  9. package/dist/crypto/decrypt.d.ts +41 -0
  10. package/dist/crypto/decrypt.js +112 -0
  11. package/dist/crypto/decrypt.js.map +1 -0
  12. package/dist/crypto/keypair.d.ts +39 -0
  13. package/dist/crypto/keypair.js +109 -0
  14. package/dist/crypto/keypair.js.map +1 -0
  15. package/dist/crypto/signature.d.ts +28 -0
  16. package/dist/crypto/signature.js +89 -0
  17. package/dist/crypto/signature.js.map +1 -0
  18. package/dist/crypto/utils.d.ts +28 -0
  19. package/dist/crypto/utils.js +60 -0
  20. package/dist/crypto/utils.js.map +1 -0
  21. package/dist/email.d.ts +63 -0
  22. package/dist/email.js +186 -0
  23. package/dist/email.js.map +1 -0
  24. package/dist/http/api-client.d.ts +145 -0
  25. package/dist/http/api-client.js +242 -0
  26. package/dist/http/api-client.js.map +1 -0
  27. package/dist/inbox.d.ts +120 -0
  28. package/dist/inbox.js +243 -0
  29. package/dist/inbox.js.map +1 -0
  30. package/dist/index.d.ts +14 -0
  31. package/dist/index.js +17 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/strategies/delivery-strategy.d.ts +29 -0
  34. package/dist/strategies/delivery-strategy.js +2 -0
  35. package/dist/strategies/delivery-strategy.js.map +1 -0
  36. package/dist/strategies/polling-strategy.d.ts +36 -0
  37. package/dist/strategies/polling-strategy.js +146 -0
  38. package/dist/strategies/polling-strategy.js.map +1 -0
  39. package/dist/strategies/sse-strategy.d.ts +49 -0
  40. package/dist/strategies/sse-strategy.js +266 -0
  41. package/dist/strategies/sse-strategy.js.map +1 -0
  42. package/dist/types/index.d.ts +434 -0
  43. package/dist/types/index.js +127 -0
  44. package/dist/types/index.js.map +1 -0
  45. package/dist/utils/email-utils.d.ts +19 -0
  46. package/dist/utils/email-utils.js +92 -0
  47. package/dist/utils/email-utils.js.map +1 -0
  48. package/dist/utils/sleep.d.ts +6 -0
  49. package/dist/utils/sleep.js +9 -0
  50. package/dist/utils/sleep.js.map +1 -0
  51. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Antero
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,612 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="./assets/logo-dark.svg">
3
+ <source media="(prefers-color-scheme: light)" srcset="./assets/logo-light.svg">
4
+ <img alt="VaultSandbox" src="./assets/logo-dark.svg">
5
+ </picture>
6
+
7
+ # @vaultsandbox/client
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@vaultsandbox/client.svg)](https://www.npmjs.com/package/@vaultsandbox/client)
10
+ [![CI](https://github.com/vaultsandbox/client-node/actions/workflows/ci.yml/badge.svg)](https://github.com/vaultsandbox/client-node/actions/workflows/ci.yml)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
13
+
14
+ **Production-like email testing. Self-hosted & secure.**
15
+
16
+ The official Node.js SDK for [VaultSandbox Gateway](https://github.com/vaultsandbox/gateway) — a secure, receive-only SMTP server for QA/testing environments. This SDK abstracts quantum-safe encryption complexity, making email testing workflows transparent and effortless.
17
+
18
+ Stop mocking your email stack. If your app sends real emails in production, it must send real emails in testing. VaultSandbox provides isolated inboxes that behave exactly like production without exposing a single byte of customer data.
19
+
20
+ > **Node.js 20+** required. Not intended for browsers or edge runtimes.
21
+
22
+ ## Why VaultSandbox?
23
+
24
+ | Feature | Simple Mocks | Public SaaS | **VaultSandbox** |
25
+ | :------------------ | :--------------- | :----------- | :------------------ |
26
+ | **TLS/SSL** | Ignored/Disabled | Partial | **Real ACME certs** |
27
+ | **Data Privacy** | Local only | Shared cloud | **Private VPC** |
28
+ | **Inbound Mail** | Outbound only | Yes | **Real MX** |
29
+ | **Auth (SPF/DKIM)** | None | Limited | **Full Validation** |
30
+ | **Crypto** | Plaintext | Varies | **Zero-Knowledge** |
31
+
32
+ ## Features
33
+
34
+ - **Quantum-Safe Encryption** — Automatic ML-KEM-768 (Kyber768) key encapsulation + AES-256-GCM encryption
35
+ - **Zero Crypto Knowledge Required** — All cryptographic operations are invisible to the user
36
+ - **Real-Time Email Delivery** — SSE-based delivery with smart polling fallback
37
+ - **Built for CI/CD** — Deterministic tests without sleeps, polling, or flakiness
38
+ - **Full Email Access** — Decrypt and access email content, headers, links, and attachments
39
+ - **Email Authentication** — Built-in SPF/DKIM/DMARC validation helpers
40
+ - **Type-Safe** — Full TypeScript support with comprehensive type definitions
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @vaultsandbox/client
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```javascript
51
+ import { VaultSandboxClient } from '@vaultsandbox/client';
52
+
53
+ // Initialize client with your API key
54
+ const client = new VaultSandboxClient({
55
+ url: 'https://smtp.vaultsandbox.com',
56
+ apiKey: 'your-api-key',
57
+ });
58
+
59
+ // Create inbox (keypair generated automatically)
60
+ const inbox = await client.createInbox();
61
+ console.log(`Send email to: ${inbox.emailAddress}`);
62
+
63
+ // Wait for email with timeout
64
+ const email = await inbox.waitForEmail({
65
+ timeout: 30000, // 30 seconds
66
+ subject: /Test/, // Optional filter
67
+ });
68
+
69
+ // Email is already decrypted - just use it!
70
+ console.log('From:', email.from);
71
+ console.log('Subject:', email.subject);
72
+ console.log('Text:', email.text);
73
+ console.log('HTML:', email.html);
74
+
75
+ // Cleanup
76
+ await inbox.delete();
77
+ ```
78
+
79
+ ## Usage Examples
80
+
81
+ ### Testing Password Reset Emails
82
+
83
+ ```javascript
84
+ import { VaultSandboxClient } from '@vaultsandbox/client';
85
+
86
+ const client = new VaultSandboxClient({ url, apiKey });
87
+ const inbox = await client.createInbox();
88
+
89
+ // Trigger password reset in your app (replace with your own implementation)
90
+ await yourApp.requestPasswordReset(inbox.emailAddress);
91
+
92
+ // Wait for and validate the reset email
93
+ const email = await inbox.waitForEmail({
94
+ timeout: 10000,
95
+ subject: /Reset your password/,
96
+ });
97
+
98
+ // Extract reset link
99
+ const resetLink = email.links.find((url) => url.includes('/reset-password'));
100
+ console.log('Reset link:', resetLink);
101
+
102
+ // Validate email authentication
103
+ const authValidation = email.authResults.validate();
104
+ // In a real test, this may not pass if the sender isn't fully configured.
105
+ // A robust check verifies the validation was performed and has the correct shape.
106
+ expect(typeof authValidation.passed).toBe('boolean');
107
+ expect(Array.isArray(authValidation.failures)).toBe(true);
108
+
109
+ await inbox.delete();
110
+ ```
111
+
112
+ ### Testing Email Authentication (SPF/DKIM/DMARC)
113
+
114
+ ```javascript
115
+ const email = await inbox.waitForEmail({ timeout: 5000 });
116
+ const validation = email.authResults.validate();
117
+
118
+ if (!validation.passed) {
119
+ console.error('Email authentication failed:');
120
+ validation.failures.forEach((reason) => {
121
+ console.error(` - ${reason}`);
122
+ });
123
+ }
124
+
125
+ // Or check individual results. Statuses can vary based on the sending source.
126
+ if (email.authResults.spf?.status) {
127
+ expect(email.authResults.spf.status).toMatch(/pass|fail|softfail|neutral|temperror|permerror/);
128
+ }
129
+ if (email.authResults.dkim) {
130
+ expect(email.authResults.dkim.length).toBeGreaterThan(0);
131
+ }
132
+ if (email.authResults.dmarc?.status) {
133
+ expect(email.authResults.dmarc.status).toMatch(/pass|fail|neutral|temperror|permerror/);
134
+ }
135
+ ```
136
+
137
+ ### Extracting and Validating Links
138
+
139
+ ```javascript
140
+ const email = await inbox.waitForEmail({ subject: /Verify your email/ });
141
+
142
+ // All links are automatically extracted
143
+ const verifyLink = email.links.find((url) => url.includes('/verify'));
144
+ expect(verifyLink).toBeDefined();
145
+ expect(verifyLink).toContain('https://');
146
+
147
+ // Test the verification flow
148
+ const response = await fetch(verifyLink);
149
+ expect(response.ok).toBe(true);
150
+ ```
151
+
152
+ ### Working with Email Attachments
153
+
154
+ Email attachments are automatically decrypted and available as `Uint8Array` buffers, ready to be processed or saved.
155
+
156
+ ```javascript
157
+ import { writeFileSync } from 'fs';
158
+
159
+ const email = await inbox.waitForEmail({ subject: /Documents Attached/ });
160
+
161
+ // Access attachments array
162
+ console.log(`Found ${email.attachments.length} attachments`);
163
+
164
+ // Iterate through attachments
165
+ for (const attachment of email.attachments) {
166
+ console.log(`Filename: ${attachment.filename}`);
167
+ console.log(`Content-Type: ${attachment.contentType}`);
168
+ console.log(`Size: ${attachment.size} bytes`);
169
+
170
+ if (!attachment.content) continue;
171
+
172
+ // Decode text-based attachments
173
+ if (attachment.contentType.includes('text')) {
174
+ const textContent = new TextDecoder().decode(attachment.content);
175
+ console.log('Content:', textContent);
176
+ }
177
+
178
+ // Parse JSON attachments
179
+ if (attachment.contentType.includes('json')) {
180
+ const jsonContent = new TextDecoder().decode(attachment.content);
181
+ const data = JSON.parse(jsonContent);
182
+ console.log('Parsed data:', data);
183
+ }
184
+
185
+ // Save binary files to disk
186
+ if (attachment.contentType.includes('pdf') || attachment.contentType.includes('image')) {
187
+ writeFileSync(`./downloads/${attachment.filename}`, attachment.content);
188
+ console.log(`Saved ${attachment.filename}`);
189
+ }
190
+ }
191
+
192
+ // Find and verify specific attachment in tests
193
+ const pdfAttachment = email.attachments.find((att) => att.filename === 'invoice.pdf');
194
+ expect(pdfAttachment).toBeDefined();
195
+ expect(pdfAttachment!.contentType).toBe('application/pdf');
196
+ expect(pdfAttachment!.size).toBeGreaterThan(0);
197
+
198
+ // Verify attachment content exists and has expected size
199
+ if (pdfAttachment?.content) {
200
+ expect(pdfAttachment.content.length).toBe(pdfAttachment.size);
201
+ }
202
+ ```
203
+
204
+ ### Testing with Jest
205
+
206
+ ```javascript
207
+ describe('Email Flow', () => {
208
+ let client, inbox;
209
+
210
+ beforeEach(async () => {
211
+ client = new VaultSandboxClient({ url, apiKey });
212
+ inbox = await client.createInbox();
213
+ });
214
+
215
+ afterEach(async () => {
216
+ await inbox?.delete();
217
+ });
218
+
219
+ test('should receive welcome email', async () => {
220
+ await sendWelcomeEmail(inbox.emailAddress);
221
+
222
+ const email = await inbox.waitForEmail({
223
+ timeout: 5000,
224
+ subject: /Welcome/,
225
+ });
226
+
227
+ expect(email.from).toBe('noreply@example.com');
228
+ expect(email.text).toContain('Thank you for signing up');
229
+ });
230
+ });
231
+ ```
232
+
233
+ ### Waiting for Multiple Emails
234
+
235
+ When testing scenarios that send multiple emails, use `waitForEmailCount()` instead of arbitrary timeouts for faster and more reliable tests:
236
+
237
+ ```javascript
238
+ test('should receive multiple notification emails', async () => {
239
+ // Send multiple emails
240
+ await sendNotifications(inbox.emailAddress, 3);
241
+
242
+ // Wait for all 3 emails to arrive (polls every 1s by default)
243
+ await inbox.waitForEmailCount(3, { timeout: 30000 });
244
+
245
+ // Now list and verify all emails
246
+ const emails = await inbox.listEmails();
247
+ expect(emails.length).toBe(3);
248
+ expect(emails[0].subject).toContain('Notification');
249
+ });
250
+ ```
251
+
252
+ ### Real-time Monitoring
253
+
254
+ For scenarios where you need to process emails as they arrive without blocking, you can use the `onNewEmail` subscription.
255
+
256
+ ```javascript
257
+ import { VaultSandboxClient } from '@vaultsandbox/client';
258
+
259
+ const client = new VaultSandboxClient({ url, apiKey });
260
+ const inbox = await client.createInbox();
261
+
262
+ console.log(`Watching for emails at: ${inbox.emailAddress}`);
263
+
264
+ // Subscribe to new emails
265
+ const subscription = inbox.onNewEmail((email) => {
266
+ console.log(`New email received: "${email.subject}"`);
267
+ // Process the email here...
268
+ });
269
+
270
+ // To stop listening for emails later:
271
+ // subscription.unsubscribe();
272
+ ```
273
+
274
+ ## API Reference
275
+
276
+ ### VaultSandboxClient
277
+
278
+ The main client class for interacting with the VaultSandbox Gateway.
279
+
280
+ #### Constructor
281
+
282
+ ```typescript
283
+ new VaultSandboxClient(config: ClientConfig)
284
+ ```
285
+
286
+ **ClientConfig:**
287
+
288
+ - `url: string` - Gateway URL
289
+ - `apiKey: string` - Your API key
290
+ - `strategy?: 'sse' | 'polling' | 'auto'` - Delivery strategy (default: 'auto')
291
+ - `pollingInterval?: number` - Polling interval in ms (default: 2000)
292
+ - `maxRetries?: number` - Max retry attempts for HTTP requests (default: 3)
293
+ - `retryDelay?: number` - Delay in ms between retry attempts (default: 1000)
294
+ - `retryOn?: number[]` - HTTP status codes that trigger a retry (default: [408, 429, 500, 502, 503, 504])
295
+ - `sseReconnectInterval?: number` - Initial delay in ms before SSE reconnection (default: 5000)
296
+ - `sseMaxReconnectAttempts?: number` - Max SSE reconnection attempts (default: 10)
297
+
298
+ #### Methods
299
+
300
+ - `createInbox(options?: CreateInboxOptions): Promise<Inbox>` - Creates a new inbox
301
+ - `deleteAllInboxes(): Promise<number>` - Deletes all inboxes for this API key
302
+ - `getServerInfo(): Promise<ServerInfo>` - Gets server information
303
+ - `checkKey(): Promise<boolean>` - Validates API key
304
+ - `monitorInboxes(inboxes: Inbox[]): InboxMonitor` - Monitors multiple inboxes and emits an `email` event when a new email arrives in any of them. Returns a monitor with an `unsubscribe()` method.
305
+ - `exportInbox(inboxOrEmail: Inbox | string): ExportedInboxData` - Exports an inbox's data for backup or sharing
306
+ - `importInbox(data: ExportedInboxData): Promise<Inbox>` - Imports an inbox from exported data
307
+ - `exportInboxToFile(inboxOrEmail: Inbox | string, filePath: string): void` - Exports an inbox to a JSON file
308
+ - `importInboxFromFile(filePath: string): Promise<Inbox>` - Imports an inbox from a JSON file
309
+ - `close(): Promise<void>` - Closes the client, terminates any active SSE or polling connections, and cleans up resources.
310
+
311
+ **Inbox Import/Export:** For advanced use cases like test reproducibility or sharing inboxes between environments, you can export an inbox (including its encryption keys) to a JSON file and import it later. This allows you to persist inboxes across test runs or share them with other tools.
312
+
313
+ **Testing with an Exported File:**
314
+ A manual test script is available at `tests/manual/check-inbox.manual-test.ts` to quickly test importing an inbox from a file and monitoring for new emails.
315
+
316
+ 1. **Export Inbox:** From the VaultSandbox Web UI, export your inbox to a JSON file.
317
+ 2. **Place File:** Create a `tmp` directory at the root of this project and place the exported file there (e.g., `tmp/my-inbox.json`).
318
+ 3. **Update Script:** Open `tests/manual/check-inbox.manual-test.ts` and change the `jsonPath` variable to point to your file.
319
+ 4. **Run Test:** Execute the script using `tsx`:
320
+ ```bash
321
+ npx tsx tests/manual/check-inbox.manual-test.ts
322
+ ```
323
+ The script will import the inbox and print a message whenever a new email is received.
324
+
325
+ ### InboxMonitor
326
+
327
+ An event emitter for monitoring multiple inboxes simultaneously. Returned by `VaultSandboxClient.monitorInboxes()`.
328
+
329
+ #### Events
330
+
331
+ - `email(inbox: Inbox, email: Email)` - Emitted when a new email arrives in any monitored inbox
332
+
333
+ #### Methods
334
+
335
+ - `on(event: 'email', listener: (inbox: Inbox, email: Email) => void): this` - Subscribe to email events
336
+ - `unsubscribe(): void` - Unsubscribe from all inboxes and stop monitoring
337
+
338
+ #### Example
339
+
340
+ ```typescript
341
+ const inbox1 = await client.createInbox();
342
+ const inbox2 = await client.createInbox();
343
+
344
+ const monitor = client.monitorInboxes([inbox1, inbox2]);
345
+
346
+ console.log(`Monitoring inboxes: ${inbox1.emailAddress}, ${inbox2.emailAddress}`);
347
+
348
+ monitor.on('email', (inbox, email) => {
349
+ console.log(`New email in ${inbox.emailAddress}: ${email.subject}`);
350
+ // Further processing...
351
+ });
352
+
353
+ // Later, to stop monitoring all inboxes:
354
+ // monitor.unsubscribe();
355
+ ```
356
+
357
+ ### Inbox
358
+
359
+ Represents a single email inbox.
360
+
361
+ #### Properties
362
+
363
+ - `emailAddress: string` - The inbox email address
364
+ - `inboxHash: string` - Unique inbox identifier
365
+ - `expiresAt: Date` - When the inbox expires
366
+
367
+ #### Methods
368
+
369
+ - `listEmails(): Promise<Email[]>` - Lists all emails (decrypted)
370
+ - `getEmail(emailId: string): Promise<Email>` - Gets a specific email
371
+ - `waitForEmail(options: WaitOptions): Promise<Email>` - Waits for an email matching criteria
372
+ - `waitForEmailCount(count: number, options?: WaitForCountOptions): Promise<void>` - Waits until the inbox has at least the specified number of emails. More efficient than using arbitrary timeouts in tests.
373
+ - `onNewEmail(callback: (email: Email) => void): Subscription` - Subscribes to new emails in real-time. Returns a subscription with an `unsubscribe()` method.
374
+ - `getSyncStatus(): Promise<SyncStatus>` - Gets inbox sync status
375
+ - `getRawEmail(emailId: string): Promise<RawEmail>` - Gets the raw, decrypted source of a specific email.
376
+ - `markEmailAsRead(emailId: string): Promise<void>` - Marks email as read
377
+ - `deleteEmail(emailId: string): Promise<void>` - Deletes an email
378
+ - `delete(): Promise<void>` - Deletes this inbox
379
+ - `export(): ExportedInboxData` - Exports inbox data and key material for backup/sharing (treat output as sensitive)
380
+
381
+ ### Email
382
+
383
+ Represents a decrypted email.
384
+
385
+ #### Properties
386
+
387
+ - `id: string` - Email ID
388
+ - `from: string` - Sender address
389
+ - `to: string[]` - Recipient addresses
390
+ - `subject: string` - Email subject
391
+ - `text: string | null` - Plain text content
392
+ - `html: string | null` - HTML content
393
+ - `receivedAt: Date` - When the email was received
394
+ - `isRead: boolean` - Read status
395
+ - `links: string[]` - Extracted URLs from email
396
+ - `headers: Record<string, unknown>` - All email headers
397
+ - `attachments: AttachmentData[]` - Email attachments
398
+ - `authResults: AuthResults` - Email authentication results
399
+ - `metadata: Record<string, unknown>` - Other metadata associated with the email
400
+
401
+ #### Methods
402
+
403
+ - `markAsRead(): Promise<void>` - Marks this email as read
404
+ - `delete(): Promise<void>` - Deletes this email
405
+ - `getRaw(): Promise<RawEmail>` - Gets raw email source
406
+
407
+ ### AuthResults
408
+
409
+ Returned by `email.authResults`, this object contains email authentication results (SPF, DKIM, DMARC) and a validation helper.
410
+
411
+ #### Properties
412
+
413
+ - `spf?: SPFResult` - SPF result
414
+ - `dkim?: DKIMResult[]` - All DKIM results
415
+ - `dmarc?: DMARCResult` - DMARC result
416
+ - `reverseDns?: ReverseDNSResult` - Reverse DNS result
417
+
418
+ #### Methods
419
+
420
+ - `validate(): AuthValidation` - Validates all authentication results and returns a summary object with `passed`, per-check booleans (`spfPassed`, `dkimPassed`, `dmarcPassed`, `reverseDnsPassed`), and a list of `failures`.
421
+
422
+ ### CreateInboxOptions
423
+
424
+ Options for creating an inbox with `client.createInbox()`.
425
+
426
+ **Properties:**
427
+
428
+ - `ttl?: number` - Time-to-live for the inbox in seconds (default: server-defined).
429
+ - `emailAddress?: string` - A specific email address to request (e.g., `test@inbox.vaultsandbox.com`). If unavailable, the server will generate one.
430
+
431
+ ### WaitOptions
432
+
433
+ Options for waiting for emails with `inbox.waitForEmail()`.
434
+
435
+ **Properties:**
436
+
437
+ - `timeout?: number` - Maximum time to wait in milliseconds (default: 30000)
438
+ - `pollInterval?: number` - Polling interval in milliseconds (default: 2000)
439
+ - `subject?: string | RegExp` - Filter emails by subject
440
+ - `from?: string | RegExp` - Filter emails by sender address
441
+ - `predicate?: (email: Email) => boolean` - Custom filter function
442
+
443
+ **Example:**
444
+
445
+ ```typescript
446
+ // Wait for email with specific subject
447
+ const email = await inbox.waitForEmail({
448
+ timeout: 10000,
449
+ subject: /Password Reset/,
450
+ });
451
+
452
+ // Wait with custom predicate
453
+ const email = await inbox.waitForEmail({
454
+ timeout: 15000,
455
+ predicate: (email) => email.to.includes('user@example.com'),
456
+ });
457
+ ```
458
+
459
+ ### WaitForCountOptions
460
+
461
+ Options for waiting for a specific number of emails with `inbox.waitForEmailCount()`.
462
+
463
+ **Properties:**
464
+
465
+ - `timeout?: number` - Maximum time to wait in milliseconds (default: 30000)
466
+ - `pollInterval?: number` - Polling interval in milliseconds (default: 1000)
467
+
468
+ **Example:**
469
+
470
+ ```typescript
471
+ // Trigger multiple emails
472
+ await sendMultipleNotifications(inbox.emailAddress);
473
+
474
+ // Wait for all 3 to arrive
475
+ await inbox.waitForEmailCount(3, { timeout: 30000 });
476
+
477
+ const emails = await inbox.listEmails();
478
+ expect(emails.length).toBe(3);
479
+ ```
480
+
481
+ ## Error Handling
482
+
483
+ The SDK is designed to be resilient and provide clear feedback when issues occur. It includes automatic retries for transient network and server errors, and throws specific, catchable errors for different failure scenarios.
484
+
485
+ All custom errors thrown by the SDK extend from the base `VaultSandboxError` class, so you can catch all SDK-specific errors with a single `catch` block if needed.
486
+
487
+ ### Automatic Retries
488
+
489
+ By default, the client automatically retries failed HTTP requests that result in one of the following status codes: `408`, `429`, `500`, `502`, `503`, `504`. This helps mitigate transient network or server-side issues.
490
+
491
+ The retry behavior can be configured via the `VaultSandboxClient` constructor:
492
+
493
+ - `maxRetries`: The maximum number of retry attempts (default: `3`).
494
+ - `retryDelay`: The base delay in milliseconds between retries (default: `1000`). The delay uses exponential backoff.
495
+ - `retryOn`: An array of HTTP status codes that should trigger a retry.
496
+
497
+ ### Custom Error Types
498
+
499
+ The following custom error classes may be thrown:
500
+
501
+ - **`ApiError`**: Thrown for API-level errors (e.g., invalid request, permission denied). Includes a `statusCode` property.
502
+ - **`NetworkError`**: Thrown when there is a network-level failure (e.g., the client cannot connect to the server).
503
+ - **`TimeoutError`**: Thrown by methods like `waitForEmail` and `waitForEmailCount` when the timeout is reached before the condition is met.
504
+ - **`InboxNotFoundError`**: Thrown when an operation targets an inbox that does not exist (HTTP 404).
505
+ - **`EmailNotFoundError`**: Thrown when an operation targets an email that does not exist (HTTP 404).
506
+ - **`InboxAlreadyExistsError`**: Thrown when attempting to import an inbox that already exists in the client.
507
+ - **`InvalidImportDataError`**: Thrown when imported inbox data fails validation (missing fields, invalid keys, server mismatch, etc.).
508
+ - **`DecryptionError`**: Thrown if the client fails to decrypt an email. This is rare and may indicate data corruption or a bug.
509
+ - **`SignatureVerificationError`**: Thrown if the cryptographic signature of a message from the server cannot be verified. This is a critical error that may indicate a man-in-the-middle (MITM) attack.
510
+ - **`SSEError`**: Thrown for errors related to the Server-Sent Events (SSE) connection.
511
+
512
+ ### Example
513
+
514
+ You can use a `try...catch` block to handle errors and use `instanceof` to check for specific error types.
515
+
516
+ ```javascript
517
+ import { VaultSandboxClient, ApiError, TimeoutError, VaultSandboxError } from '@vaultsandbox/client';
518
+
519
+ const client = new VaultSandboxClient({ url, apiKey });
520
+
521
+ try {
522
+ const inbox = await client.createInbox();
523
+ console.log(`Send email to: ${inbox.emailAddress}`);
524
+
525
+ // This might throw a TimeoutError
526
+ const email = await inbox.waitForEmail({ timeout: 5000 });
527
+
528
+ console.log('Email received:', email.subject);
529
+ await inbox.delete();
530
+ } catch (error) {
531
+ if (error instanceof TimeoutError) {
532
+ console.error('Timed out waiting for email:', error.message);
533
+ } else if (error instanceof ApiError) {
534
+ console.error(`API Error (${error.statusCode}):`, error.message);
535
+ } else if (error instanceof VaultSandboxError) {
536
+ // Catch any other SDK-specific error
537
+ console.error('An unexpected SDK error occurred:', error.message);
538
+ } else {
539
+ // Handle other unexpected errors
540
+ console.error('An unexpected error occurred:', error);
541
+ }
542
+ }
543
+ ```
544
+
545
+ ## Requirements
546
+
547
+ - Node.js >= 20.0.0 (tested on Node 20 and 22; ES2022 target)
548
+ - Not supported in browsers/edge runtimes
549
+ - VaultSandbox Gateway server
550
+ - Valid API key
551
+
552
+ ## Testing
553
+
554
+ ```bash
555
+ # Run unit tests
556
+ npm test
557
+
558
+ # Run tests in watch mode
559
+ npm run test:watch
560
+
561
+ # Run tests with coverage
562
+ npm run test:cov
563
+ ```
564
+
565
+ ## Building
566
+
567
+ ```bash
568
+ # Build TypeScript
569
+ npm run build
570
+
571
+ # Clean build artifacts
572
+ npm run clean
573
+ ```
574
+
575
+ ## Architecture
576
+
577
+ The SDK is built on several layers:
578
+
579
+ 1. **Crypto Layer**: Handles ML-KEM-768 keypair generation, AES-256-GCM encryption/decryption, and ML-DSA-65 signature verification
580
+ 2. **HTTP Layer**: REST API client with automatic retry and error handling
581
+ 3. **Domain Layer**: Email, Inbox, and Client classes with intuitive APIs
582
+ 4. **Strategy Layer**: SSE and polling strategies for email delivery
583
+
584
+ All cryptographic operations are performed transparently - developers never need to handle keys, encryption, or signatures directly.
585
+
586
+ ## Security
587
+
588
+ - Cryptography: ML-KEM-768 (Kyber768) for key encapsulation + AES-256-GCM for payload encryption, with HKDF-SHA-512 key derivation.
589
+ - Signatures: ML-DSA-65 (Dilithium3) signatures are verified **before** any decryption using the gateway-provided transcript context (`vaultsandbox:email:v1` today).
590
+ - Threat model: protects confidentiality/integrity of gateway responses and detects tampering/mitm. Skipping signature verification defeats these guarantees.
591
+ - Key handling: inbox keypairs stay in memory only; exported inbox data contains secrets and must be treated as sensitive.
592
+ - Validation: signature verification failures throw `SignatureVerificationError`; decryption issues throw `DecryptionError`. Always surface these in logs/alerts for investigation.
593
+
594
+ ## Related
595
+
596
+ - [VaultSandbox Gateway](https://github.com/vaultsandbox/gateway) — The self-hosted SMTP server this SDK connects to
597
+ - [VaultSandbox Documentation](https://vaultsandbox.dev) — Full documentation and guides
598
+
599
+ ## Support
600
+
601
+ - [Documentation](https://vaultsandbox.dev/client-node/installation)
602
+ - [Issue Tracker](https://github.com/vaultsandbox/client-node/issues)
603
+ - [Discussions](https://github.com/vaultsandbox/client-node/discussions)
604
+ - [Website](https://www.vaultsandbox.com)
605
+
606
+ ## Contributing
607
+
608
+ Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) before submitting PRs.
609
+
610
+ ## License
611
+
612
+ MIT — see [LICENSE](LICENSE) for details.