@bernierllc/email-sendgrid-plugin 1.0.1

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 (58) hide show
  1. package/.eslintrc.js +27 -0
  2. package/README.md +432 -0
  3. package/__tests__/api/rate-limiter.test.ts +125 -0
  4. package/__tests__/converters/sender-converter.test.ts +148 -0
  5. package/__tests__/converters/template-converter.test.ts +230 -0
  6. package/__tests__/plugin/sender-manager.test.ts +135 -0
  7. package/__tests__/plugin/sendgrid-plugin.test.ts +195 -0
  8. package/__tests__/plugin/template-manager.test.ts +110 -0
  9. package/__tests__/safety/ownership-validator.test.ts +130 -0
  10. package/__tests__/safety/prefix-enforcer.test.ts +136 -0
  11. package/dist/api/errors.d.ts +20 -0
  12. package/dist/api/errors.js +50 -0
  13. package/dist/api/rate-limiter.d.ts +17 -0
  14. package/dist/api/rate-limiter.js +58 -0
  15. package/dist/api/sendgrid-client.d.ts +21 -0
  16. package/dist/api/sendgrid-client.js +67 -0
  17. package/dist/converters/sender-converter.d.ts +6 -0
  18. package/dist/converters/sender-converter.js +40 -0
  19. package/dist/converters/template-converter.d.ts +11 -0
  20. package/dist/converters/template-converter.js +76 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +42 -0
  23. package/dist/plugin/sender-manager.d.ts +20 -0
  24. package/dist/plugin/sender-manager.js +124 -0
  25. package/dist/plugin/sendgrid-plugin.d.ts +60 -0
  26. package/dist/plugin/sendgrid-plugin.js +204 -0
  27. package/dist/plugin/sync-manager.d.ts +19 -0
  28. package/dist/plugin/sync-manager.js +94 -0
  29. package/dist/plugin/template-manager.d.ts +25 -0
  30. package/dist/plugin/template-manager.js +152 -0
  31. package/dist/safety/ownership-validator.d.ts +15 -0
  32. package/dist/safety/ownership-validator.js +42 -0
  33. package/dist/safety/prefix-enforcer.d.ts +8 -0
  34. package/dist/safety/prefix-enforcer.js +34 -0
  35. package/dist/types/config-types.d.ts +24 -0
  36. package/dist/types/config-types.js +9 -0
  37. package/dist/types/plugin-types.d.ts +79 -0
  38. package/dist/types/plugin-types.js +9 -0
  39. package/dist/types/sendgrid-types.d.ts +56 -0
  40. package/dist/types/sendgrid-types.js +9 -0
  41. package/jest.config.js +25 -0
  42. package/package.json +42 -0
  43. package/src/api/errors.ts +49 -0
  44. package/src/api/rate-limiter.ts +79 -0
  45. package/src/api/sendgrid-client.ts +86 -0
  46. package/src/converters/sender-converter.ts +41 -0
  47. package/src/converters/template-converter.ts +97 -0
  48. package/src/index.ts +58 -0
  49. package/src/plugin/sender-manager.ts +166 -0
  50. package/src/plugin/sendgrid-plugin.ts +316 -0
  51. package/src/plugin/sync-manager.ts +115 -0
  52. package/src/plugin/template-manager.ts +240 -0
  53. package/src/safety/ownership-validator.ts +55 -0
  54. package/src/safety/prefix-enforcer.ts +37 -0
  55. package/src/types/config-types.ts +34 -0
  56. package/src/types/plugin-types.ts +97 -0
  57. package/src/types/sendgrid-types.ts +69 -0
  58. package/tsconfig.json +18 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ extends: [
12
+ 'eslint:recommended',
13
+ 'plugin:@typescript-eslint/recommended',
14
+ ],
15
+ parserOptions: {
16
+ ecmaVersion: 2020,
17
+ sourceType: 'module',
18
+ },
19
+ rules: {
20
+ '@typescript-eslint/no-explicit-any': 'warn',
21
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
22
+ '@typescript-eslint/no-unused-vars': ['error', {
23
+ 'argsIgnorePattern': '^_+',
24
+ 'varsIgnorePattern': '^_+'
25
+ }],
26
+ },
27
+ };
package/README.md ADDED
@@ -0,0 +1,432 @@
1
+ # @bernierllc/email-sendgrid-plugin
2
+
3
+ SendGrid-specific template management, sender verification, and advanced features plugin for email template services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/email-sendgrid-plugin
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ This package provides a comprehensive plugin for integrating SendGrid's template management and sender verification features into your email template service. It includes:
14
+
15
+ - **Template CRUD Operations** - Create, read, update, and delete SendGrid templates
16
+ - **Verified Sender Management** - Manage and validate SendGrid verified senders
17
+ - **Template Synchronization** - Bi-directional sync between local and SendGrid templates
18
+ - **Safety Features** - Shared account protection and template ownership validation
19
+ - **Rate Limiting** - Built-in API rate limiting to respect SendGrid quotas
20
+ - **Template Versioning** - Support for SendGrid template versions and activation
21
+
22
+ ## Key Features
23
+
24
+ ### Template Management
25
+ - Full CRUD operations with SendGrid API
26
+ - Template versioning and activation
27
+ - Shared account safety (application-scoped operations)
28
+ - Template prefix enforcement
29
+ - Conflict detection and resolution
30
+
31
+ ### Verified Sender Integration
32
+ - Sender verification status checking
33
+ - Domain matching and validation
34
+ - Automatic sender selection
35
+ - Sender configuration management
36
+
37
+ ### Safety and Security
38
+ - Application-scoped operations (never access non-app templates)
39
+ - Template prefix enforcement
40
+ - Shared account protection
41
+ - Operation auditing and logging
42
+ - Strict/permissive safety modes
43
+
44
+ ## Usage
45
+
46
+ ### Basic Setup
47
+
48
+ ```typescript
49
+ import { SendGridTemplatePlugin } from '@bernierllc/email-sendgrid-plugin';
50
+
51
+ const plugin = new SendGridTemplatePlugin({
52
+ apiKey: process.env.SENDGRID_API_KEY!,
53
+ templatePrefix: 'MyApp - ',
54
+ safetyMode: 'strict',
55
+ rateLimits: {
56
+ templatesPerMinute: 60,
57
+ apiCallsPerSecond: 10
58
+ }
59
+ });
60
+
61
+ // Initialize the plugin
62
+ await plugin.initialize({});
63
+ ```
64
+
65
+ ### Creating Templates
66
+
67
+ ```typescript
68
+ import { Template } from '@bernierllc/email-sendgrid-plugin';
69
+
70
+ const template: Template = {
71
+ id: 'local-123',
72
+ name: 'Welcome Email',
73
+ subject: 'Welcome to {{appName}}',
74
+ htmlContent: '<html><body><h1>Welcome {{userName}}</h1></body></html>',
75
+ textContent: 'Welcome {{userName}}',
76
+ category: 'transactional',
77
+ tags: ['welcome', 'onboarding'],
78
+ version: '1.0.0',
79
+ isActive: true,
80
+ requiredVariables: ['appName', 'userName'],
81
+ createdAt: new Date(),
82
+ updatedAt: new Date(),
83
+ createdBy: 'system',
84
+ lastModifiedBy: 'system'
85
+ };
86
+
87
+ const sendgridTemplateId = await plugin.createTemplate(template);
88
+ console.log(`Created SendGrid template: ${sendgridTemplateId}`);
89
+ ```
90
+
91
+ ### Managing Templates
92
+
93
+ ```typescript
94
+ // Get template
95
+ const template = await plugin.getTemplate('sendgrid-template-id');
96
+
97
+ // List all templates
98
+ const templates = await plugin.listTemplates();
99
+
100
+ // Update template
101
+ await plugin.updateTemplate('sendgrid-template-id', updatedTemplate);
102
+
103
+ // Delete template
104
+ await plugin.deleteTemplate('sendgrid-template-id');
105
+ ```
106
+
107
+ ### Sender Verification
108
+
109
+ ```typescript
110
+ // Get all verified senders
111
+ const senders = await plugin.getVerifiedSenders();
112
+
113
+ // Validate sender email
114
+ const validation = await plugin.validateSender('noreply@myapp.com');
115
+
116
+ if (!validation.isValid) {
117
+ console.log('Sender validation failed:');
118
+ validation.errors.forEach(error => console.log(`- ${error}`));
119
+
120
+ console.log('Suggestions:');
121
+ validation.suggestions.forEach(suggestion => console.log(`- ${suggestion}`));
122
+ }
123
+
124
+ // Select best sender for outgoing email
125
+ const bestSender = await plugin.selectBestSender('noreply@myapp.com');
126
+ if (bestSender) {
127
+ console.log(`Using verified sender: ${bestSender.email}`);
128
+ }
129
+ ```
130
+
131
+ ### Template Synchronization
132
+
133
+ ```typescript
134
+ // Sync local template to SendGrid
135
+ const syncResult = await plugin.syncToProvider(localTemplate);
136
+
137
+ if (syncResult.success) {
138
+ console.log(`Synced template ${syncResult.action}: ${syncResult.providerTemplateId}`);
139
+ } else {
140
+ console.error(`Sync failed: ${syncResult.error}`);
141
+ }
142
+
143
+ // Pull template from SendGrid
144
+ const localTemplate = await plugin.syncFromProvider('sendgrid-template-id');
145
+
146
+ // Batch synchronization
147
+ const results = await plugin.batchSyncTemplates([template1, template2, template3]);
148
+ const successful = results.filter(r => r.success).length;
149
+ console.log(`Batch sync: ${successful}/${results.length} successful`);
150
+ ```
151
+
152
+ ### Template Versioning
153
+
154
+ ```typescript
155
+ // Create new template version
156
+ const versionId = await plugin.createTemplateVersion('sendgrid-template-id', {
157
+ name: 'v2.0.0',
158
+ subject: 'Updated Subject',
159
+ htmlContent: '<html><body>Updated content</body></html>',
160
+ textContent: 'Updated content',
161
+ active: false
162
+ });
163
+
164
+ // Activate template version
165
+ await plugin.activateTemplateVersion('sendgrid-template-id', versionId);
166
+ ```
167
+
168
+ ## Configuration
169
+
170
+ ### SendGridPluginConfig
171
+
172
+ ```typescript
173
+ interface SendGridPluginConfig {
174
+ // SendGrid API key (required)
175
+ apiKey: string;
176
+
177
+ // Template prefix for shared account safety (required)
178
+ templatePrefix: string;
179
+
180
+ // Safety mode: 'strict' or 'permissive' (required)
181
+ safetyMode: 'strict' | 'permissive';
182
+
183
+ // Whether using shared SendGrid account (optional)
184
+ sharedAccount?: boolean;
185
+
186
+ // API rate limits (optional)
187
+ rateLimits?: {
188
+ templatesPerMinute: number;
189
+ apiCallsPerSecond: number;
190
+ };
191
+
192
+ // Webhook configuration (optional)
193
+ webhookConfig?: {
194
+ secret: string;
195
+ eventTypes: string[];
196
+ };
197
+ }
198
+ ```
199
+
200
+ ### Safety Modes
201
+
202
+ **Strict Mode** (Recommended):
203
+ - Only allows operations on templates registered with the application
204
+ - Throws `UnauthorizedTemplateAccessError` for unregistered templates
205
+ - Provides maximum safety in shared account environments
206
+
207
+ **Permissive Mode**:
208
+ - Allows operations on any template
209
+ - Useful for administrative tools or migration scripts
210
+ - Use with caution in shared account environments
211
+
212
+ ### Rate Limiting
213
+
214
+ The plugin includes built-in rate limiting to respect SendGrid's API quotas:
215
+
216
+ ```typescript
217
+ const plugin = new SendGridTemplatePlugin({
218
+ apiKey: process.env.SENDGRID_API_KEY!,
219
+ templatePrefix: 'MyApp - ',
220
+ safetyMode: 'strict',
221
+ rateLimits: {
222
+ templatesPerMinute: 60, // Max 60 template operations per minute
223
+ apiCallsPerSecond: 10 // Max 10 API calls per second
224
+ }
225
+ });
226
+ ```
227
+
228
+ ## Plugin Capabilities
229
+
230
+ The plugin provides information about its capabilities:
231
+
232
+ ```typescript
233
+ // Get all capabilities
234
+ const capabilities = plugin.getCapabilities();
235
+
236
+ // Check if feature is supported
237
+ if (plugin.supportsFeature('sender-verification')) {
238
+ console.log('Sender verification is supported');
239
+ }
240
+
241
+ // Get feature configuration
242
+ const config = plugin.getFeatureConfig('template-management');
243
+ console.log(`Supports versioning: ${config.supportsVersioning}`);
244
+ ```
245
+
246
+ ### Available Capabilities
247
+
248
+ - **template-management**: Full CRUD operations for SendGrid templates
249
+ - **sender-verification**: SendGrid verified sender management
250
+ - **template-analytics**: Template usage and performance analytics
251
+ - **batch-operations**: Efficient bulk template operations
252
+
253
+ ## Error Handling
254
+
255
+ The plugin provides specific error types for different failure scenarios:
256
+
257
+ ```typescript
258
+ import {
259
+ SendGridApiError,
260
+ TemplateNotFoundError,
261
+ UnauthorizedTemplateAccessError,
262
+ TemplateConflictError,
263
+ RateLimitError
264
+ } from '@bernierllc/email-sendgrid-plugin';
265
+
266
+ try {
267
+ await plugin.getTemplate('template-id');
268
+ } catch (error) {
269
+ if (error instanceof TemplateNotFoundError) {
270
+ console.log('Template does not exist');
271
+ } else if (error instanceof UnauthorizedTemplateAccessError) {
272
+ console.log('Template not owned by this application');
273
+ } else if (error instanceof SendGridApiError) {
274
+ console.log(`SendGrid API error: ${error.statusCode}`);
275
+ } else if (error instanceof RateLimitError) {
276
+ console.log('Rate limit exceeded');
277
+ }
278
+ }
279
+ ```
280
+
281
+ ## Integration with Email Template Service
282
+
283
+ This plugin is designed to work with `@bernierllc/email-template-service`:
284
+
285
+ ```typescript
286
+ import { SendGridTemplatePlugin } from '@bernierllc/email-sendgrid-plugin';
287
+ import { EmailTemplateService } from '@bernierllc/email-template-service';
288
+
289
+ const sendGridPlugin = new SendGridTemplatePlugin({
290
+ apiKey: process.env.SENDGRID_API_KEY!,
291
+ templatePrefix: 'MyApp - ',
292
+ sharedAccount: true,
293
+ safetyMode: 'strict',
294
+ rateLimits: {
295
+ templatesPerMinute: 60,
296
+ apiCallsPerSecond: 10
297
+ }
298
+ });
299
+
300
+ const templateService = new EmailTemplateService({
301
+ providers: [sendGridPlugin],
302
+ syncConfig: {
303
+ enabled: true,
304
+ providers: {
305
+ sendgrid: {
306
+ enabled: true,
307
+ strategy: 'auto',
308
+ direction: 'bidirectional'
309
+ }
310
+ }
311
+ }
312
+ });
313
+
314
+ await templateService.initialize();
315
+ ```
316
+
317
+ ## Testing
318
+
319
+ Run the test suite:
320
+
321
+ ```bash
322
+ # Run tests in watch mode
323
+ npm test
324
+
325
+ # Run tests with coverage
326
+ npm run test:coverage
327
+
328
+ # Run tests once
329
+ npm run test:run
330
+ ```
331
+
332
+ ## API Reference
333
+
334
+ ### SendGridTemplatePlugin
335
+
336
+ #### Core Methods
337
+
338
+ - `initialize(config: PluginConfig): Promise<void>` - Initialize the plugin
339
+ - `destroy(): Promise<void>` - Cleanup and destroy the plugin
340
+
341
+ #### Template Operations
342
+
343
+ - `createTemplate(template: Template): Promise<string>` - Create a new template
344
+ - `updateTemplate(id: string, template: Template): Promise<void>` - Update existing template
345
+ - `deleteTemplate(id: string): Promise<void>` - Delete a template
346
+ - `getTemplate(id: string): Promise<ProviderTemplate>` - Get template by ID
347
+ - `listTemplates(): Promise<ProviderTemplate[]>` - List all templates
348
+
349
+ #### Sender Operations
350
+
351
+ - `getVerifiedSenders(): Promise<VerifiedSender[]>` - Get all verified senders
352
+ - `validateSender(email: string): Promise<SenderValidationResult>` - Validate sender email
353
+ - `selectBestSender(email: string): Promise<VerifiedSender | null>` - Auto-select best sender
354
+
355
+ #### Synchronization
356
+
357
+ - `syncToProvider(template: Template): Promise<SyncResult>` - Sync template to SendGrid
358
+ - `syncFromProvider(id: string): Promise<Template>` - Pull template from SendGrid
359
+ - `batchSyncTemplates(templates: Template[]): Promise<SyncResult[]>` - Batch sync
360
+
361
+ #### Versioning
362
+
363
+ - `createTemplateVersion(id: string, version: TemplateVersion): Promise<string>` - Create version
364
+ - `activateTemplateVersion(id: string, versionId: string): Promise<void>` - Activate version
365
+
366
+ #### Capabilities
367
+
368
+ - `getCapabilities(): ProviderCapability[]` - Get all capabilities
369
+ - `supportsFeature(feature: string): boolean` - Check feature support
370
+ - `getFeatureConfig(feature: string): FeatureConfig | null` - Get feature config
371
+
372
+ ## Integration Status
373
+
374
+ - **Logger**: Optional - All SendGrid operations and safety events
375
+ - **Docs-Suite**: Ready - Complete API documentation
376
+ - **NeverHub**: Optional - SendGrid API health monitoring and template sync events
377
+
378
+ ### NeverHub Integration
379
+
380
+ This package supports optional NeverHub integration for enhanced observability and event-driven workflows:
381
+
382
+ ```typescript
383
+ import { SendGridTemplatePlugin } from '@bernierllc/email-sendgrid-plugin';
384
+ import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
385
+
386
+ const plugin = new SendGridTemplatePlugin({
387
+ apiKey: process.env.SENDGRID_API_KEY!,
388
+ templatePrefix: 'MyApp - ',
389
+ safetyMode: 'strict'
390
+ });
391
+
392
+ // Auto-detect NeverHub availability
393
+ const neverhub = await NeverHubAdapter.detectNeverHub()
394
+ ? new NeverHubAdapter()
395
+ : null;
396
+
397
+ if (neverhub) {
398
+ // Register plugin capabilities
399
+ await neverhub.register({
400
+ type: 'email-plugin',
401
+ name: '@bernierllc/email-sendgrid-plugin',
402
+ capabilities: ['template-management', 'sender-verification']
403
+ });
404
+
405
+ // Subscribe to template events
406
+ await neverhub.subscribe('template.updated', async (event) => {
407
+ await plugin.syncFromProvider(event.templateId);
408
+ });
409
+ }
410
+
411
+ await plugin.initialize({});
412
+ ```
413
+
414
+ The plugin works fully without NeverHub integration - all core functionality is available regardless of NeverHub presence.
415
+
416
+ ## Dependencies
417
+
418
+ - `@sendgrid/mail` - Official SendGrid SDK
419
+ - `@bernierllc/logger` - Operation logging (optional)
420
+ - `@bernierllc/retry-policy` - API call retries
421
+ - `@bernierllc/rate-limiter` - API rate limiting
422
+
423
+ ## License
424
+
425
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
426
+
427
+ ## See Also
428
+
429
+ - [@bernierllc/email-template-service](../email-template-service) - Email template service
430
+ - [@bernierllc/email-sender](../email-sender) - Email sending abstraction
431
+ - [@bernierllc/retry-policy](../retry-policy) - Retry policy utilities
432
+ - [@bernierllc/rate-limiter](../rate-limiter) - Rate limiting utilities
@@ -0,0 +1,125 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { SimpleRateLimiter } from '../../src/api/rate-limiter';
10
+ import { RateLimitError } from '../../src/api/errors';
11
+
12
+ describe('SimpleRateLimiter', () => {
13
+ describe('constructor', () => {
14
+ it('should create rate limiter with specified limits', () => {
15
+ const limiter = new SimpleRateLimiter({
16
+ maxRequestsPerSecond: 10,
17
+ maxRequestsPerMinute: 100
18
+ });
19
+
20
+ expect(limiter).toBeDefined();
21
+ expect(limiter.getRemainingCapacity()).toEqual({
22
+ perSecond: 10,
23
+ perMinute: 100
24
+ });
25
+ });
26
+ });
27
+
28
+ describe('acquire', () => {
29
+ it('should allow requests within per-second limit', async () => {
30
+ const limiter = new SimpleRateLimiter({
31
+ maxRequestsPerSecond: 5,
32
+ maxRequestsPerMinute: 100
33
+ });
34
+
35
+ // Should allow 5 requests without delay
36
+ for (let i = 0; i < 5; i++) {
37
+ await expect(limiter.acquire()).resolves.toBeUndefined();
38
+ }
39
+ });
40
+
41
+ it('should enforce per-second rate limit', async () => {
42
+ const limiter = new SimpleRateLimiter({
43
+ maxRequestsPerSecond: 2,
44
+ maxRequestsPerMinute: 100
45
+ });
46
+
47
+ // Use up per-second quota
48
+ await limiter.acquire();
49
+ await limiter.acquire();
50
+
51
+ const start = Date.now();
52
+ await limiter.acquire(); // Should wait
53
+ const elapsed = Date.now() - start;
54
+
55
+ expect(elapsed).toBeGreaterThanOrEqual(900); // Allow some margin
56
+ });
57
+
58
+ it('should throw error if per-minute limit wait time exceeds threshold', async () => {
59
+ const limiter = new SimpleRateLimiter({
60
+ maxRequestsPerSecond: 100,
61
+ maxRequestsPerMinute: 2
62
+ });
63
+
64
+ // Use up per-minute quota
65
+ await limiter.acquire();
66
+ await limiter.acquire();
67
+
68
+ // Next request should throw since wait > 5s
69
+ await expect(limiter.acquire()).rejects.toThrow(RateLimitError);
70
+ });
71
+
72
+ it.skip('should clean up old timestamps (test too slow)', async () => {
73
+ // This test would take >60s to run, skipping for CI/CD speed
74
+ // Manual verification: timestamps older than 60s are filtered out
75
+ });
76
+ });
77
+
78
+ describe('getRemainingCapacity', () => {
79
+ it('should return correct remaining capacity', async () => {
80
+ const limiter = new SimpleRateLimiter({
81
+ maxRequestsPerSecond: 5,
82
+ maxRequestsPerMinute: 50
83
+ });
84
+
85
+ // Initial capacity
86
+ expect(limiter.getRemainingCapacity()).toEqual({
87
+ perSecond: 5,
88
+ perMinute: 50
89
+ });
90
+
91
+ // After 2 requests
92
+ await limiter.acquire();
93
+ await limiter.acquire();
94
+
95
+ expect(limiter.getRemainingCapacity()).toEqual({
96
+ perSecond: 3,
97
+ perMinute: 48
98
+ });
99
+ });
100
+
101
+ it('should never return negative capacity', async () => {
102
+ const limiter = new SimpleRateLimiter({
103
+ maxRequestsPerSecond: 1,
104
+ maxRequestsPerMinute: 10
105
+ });
106
+
107
+ await limiter.acquire();
108
+
109
+ const capacity = limiter.getRemainingCapacity();
110
+ expect(capacity.perSecond).toBeGreaterThanOrEqual(0);
111
+ expect(capacity.perMinute).toBeGreaterThanOrEqual(0);
112
+ });
113
+ });
114
+
115
+ describe('release', () => {
116
+ it('should be a no-op', () => {
117
+ const limiter = new SimpleRateLimiter({
118
+ maxRequestsPerSecond: 10,
119
+ maxRequestsPerMinute: 100
120
+ });
121
+
122
+ expect(() => limiter.release()).not.toThrow();
123
+ });
124
+ });
125
+ });