@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
@@ -0,0 +1,195 @@
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 { SendGridTemplatePlugin } from '../../src/plugin/sendgrid-plugin';
10
+ import { SendGridPluginConfig } from '../../src/types/config-types';
11
+
12
+ describe('SendGridTemplatePlugin', () => {
13
+ let plugin: SendGridTemplatePlugin;
14
+ let config: SendGridPluginConfig;
15
+
16
+ beforeEach(() => {
17
+ config = {
18
+ apiKey: 'test-api-key',
19
+ templatePrefix: 'Test - ',
20
+ safetyMode: 'strict' as const,
21
+ rateLimits: {
22
+ templatesPerMinute: 60,
23
+ apiCallsPerSecond: 10
24
+ }
25
+ };
26
+
27
+ plugin = new SendGridTemplatePlugin(config);
28
+ });
29
+
30
+ describe('constructor', () => {
31
+ it('should create plugin with configuration', () => {
32
+ expect(plugin).toBeDefined();
33
+ expect(plugin.name).toBe('sendgrid-templates');
34
+ expect(plugin.version).toBe('1.0.0');
35
+ expect(plugin.providerId).toBe('sendgrid');
36
+ });
37
+ });
38
+
39
+ describe('initialize', () => {
40
+ it('should initialize plugin successfully', async () => {
41
+ await expect(plugin.initialize({})).resolves.toBeUndefined();
42
+ });
43
+
44
+ it('should be idempotent', async () => {
45
+ await plugin.initialize({});
46
+ await expect(plugin.initialize({})).resolves.toBeUndefined();
47
+ });
48
+ });
49
+
50
+ describe('destroy', () => {
51
+ it('should destroy plugin successfully', async () => {
52
+ await plugin.initialize({});
53
+ await expect(plugin.destroy()).resolves.toBeUndefined();
54
+ });
55
+
56
+ it('should handle destroy without initialization', async () => {
57
+ await expect(plugin.destroy()).resolves.toBeUndefined();
58
+ });
59
+ });
60
+
61
+ describe('getCapabilities', () => {
62
+ it('should return all supported capabilities', () => {
63
+ const capabilities = plugin.getCapabilities();
64
+
65
+ expect(capabilities).toHaveLength(4);
66
+ expect(capabilities.map(c => c.feature)).toEqual([
67
+ 'template-management',
68
+ 'sender-verification',
69
+ 'template-analytics',
70
+ 'batch-operations'
71
+ ]);
72
+ });
73
+
74
+ it('should include correct methods for each capability', () => {
75
+ const capabilities = plugin.getCapabilities();
76
+
77
+ const templateManagement = capabilities.find(c => c.feature === 'template-management');
78
+ expect(templateManagement?.methods).toContain('createTemplate');
79
+ expect(templateManagement?.methods).toContain('updateTemplate');
80
+ expect(templateManagement?.methods).toContain('deleteTemplate');
81
+
82
+ const senderVerification = capabilities.find(c => c.feature === 'sender-verification');
83
+ expect(senderVerification?.methods).toContain('getVerifiedSenders');
84
+ expect(senderVerification?.methods).toContain('validateSender');
85
+ });
86
+
87
+ it('should include configuration for each capability', () => {
88
+ const capabilities = plugin.getCapabilities();
89
+
90
+ capabilities.forEach(capability => {
91
+ expect(capability.config).toBeDefined();
92
+ expect(typeof capability.config).toBe('object');
93
+ });
94
+ });
95
+ });
96
+
97
+ describe('supportsFeature', () => {
98
+ it('should return true for supported features', () => {
99
+ expect(plugin.supportsFeature('template-management')).toBe(true);
100
+ expect(plugin.supportsFeature('sender-verification')).toBe(true);
101
+ expect(plugin.supportsFeature('template-analytics')).toBe(true);
102
+ expect(plugin.supportsFeature('batch-operations')).toBe(true);
103
+ });
104
+
105
+ it('should return false for unsupported features', () => {
106
+ expect(plugin.supportsFeature('unknown-feature')).toBe(false);
107
+ expect(plugin.supportsFeature('custom-domains')).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe('getFeatureConfig', () => {
112
+ it('should return config for supported features', () => {
113
+ const config = plugin.getFeatureConfig('template-management');
114
+
115
+ expect(config).toBeDefined();
116
+ expect(config?.supportsVersioning).toBe(true);
117
+ expect(config?.supportsActivation).toBe(true);
118
+ });
119
+
120
+ it('should return null for unsupported features', () => {
121
+ const config = plugin.getFeatureConfig('unknown-feature');
122
+
123
+ expect(config).toBeNull();
124
+ });
125
+ });
126
+
127
+ describe('validateRoute', () => {
128
+ it('should validate route successfully', () => {
129
+ const result = plugin.validateRoute({});
130
+
131
+ expect(result.isValid).toBe(true);
132
+ expect(result.errors).toEqual([]);
133
+ });
134
+ });
135
+
136
+ describe('template operations', () => {
137
+ it('should throw error when not initialized', async () => {
138
+ await expect(plugin.createTemplate({} as any)).rejects.toThrow('not initialized');
139
+ await expect(plugin.getTemplate('123')).rejects.toThrow('not initialized');
140
+ await expect(plugin.listTemplates()).rejects.toThrow('not initialized');
141
+ });
142
+ });
143
+
144
+ describe('sender operations', () => {
145
+ it('should throw error when not initialized', async () => {
146
+ await expect(plugin.getVerifiedSenders()).rejects.toThrow('not initialized');
147
+ await expect(plugin.validateSender('test@example.com')).rejects.toThrow('not initialized');
148
+ await expect(plugin.selectBestSender('test@example.com')).rejects.toThrow('not initialized');
149
+ });
150
+ });
151
+
152
+ describe('sync operations', () => {
153
+ it('should throw error when not initialized', async () => {
154
+ await expect(plugin.syncToProvider({} as any)).rejects.toThrow('not initialized');
155
+ await expect(plugin.syncFromProvider('123')).rejects.toThrow('not initialized');
156
+ await expect(plugin.batchSyncTemplates([])).rejects.toThrow('not initialized');
157
+ });
158
+ });
159
+
160
+ describe('configuration variations', () => {
161
+ it('should work with permissive safety mode', async () => {
162
+ const permissiveConfig: SendGridPluginConfig = {
163
+ ...config,
164
+ safetyMode: 'permissive'
165
+ };
166
+
167
+ const permissivePlugin = new SendGridTemplatePlugin(permissiveConfig);
168
+ await expect(permissivePlugin.initialize({})).resolves.toBeUndefined();
169
+ });
170
+
171
+ it('should work without rate limits', async () => {
172
+ const noLimitsConfig: SendGridPluginConfig = {
173
+ apiKey: 'test-key',
174
+ templatePrefix: 'Test - ',
175
+ safetyMode: 'strict'
176
+ };
177
+
178
+ const noLimitsPlugin = new SendGridTemplatePlugin(noLimitsConfig);
179
+ await expect(noLimitsPlugin.initialize({})).resolves.toBeUndefined();
180
+ });
181
+
182
+ it('should work with webhook config', async () => {
183
+ const webhookConfig: SendGridPluginConfig = {
184
+ ...config,
185
+ webhookConfig: {
186
+ secret: 'webhook-secret',
187
+ eventTypes: ['delivered', 'bounced', 'opened']
188
+ }
189
+ };
190
+
191
+ const webhookPlugin = new SendGridTemplatePlugin(webhookConfig);
192
+ await expect(webhookPlugin.initialize({})).resolves.toBeUndefined();
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,110 @@
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 { TemplateManager } from '../../src/plugin/template-manager';
10
+ import { SendGridClient } from '../../src/api/sendgrid-client';
11
+ import { OwnershipValidator } from '../../src/safety/ownership-validator';
12
+ import { PrefixEnforcer } from '../../src/safety/prefix-enforcer';
13
+ import { TemplateConverter } from '../../src/converters/template-converter';
14
+ import { Template } from '../../src/types/plugin-types';
15
+
16
+ describe('TemplateManager', () => {
17
+ let manager: TemplateManager;
18
+ let mockClient: jest.Mocked<SendGridClient>;
19
+ let ownershipValidator: OwnershipValidator;
20
+ let prefixEnforcer: PrefixEnforcer;
21
+ let converter: TemplateConverter;
22
+
23
+ beforeEach(() => {
24
+ mockClient = {
25
+ request: jest.fn(),
26
+ isSuccessResponse: jest.fn(),
27
+ parseResponseBody: jest.fn()
28
+ } as any;
29
+
30
+ ownershipValidator = new OwnershipValidator('strict');
31
+ prefixEnforcer = new PrefixEnforcer('Test - ');
32
+ converter = new TemplateConverter(prefixEnforcer);
33
+
34
+ manager = new TemplateManager(
35
+ mockClient,
36
+ ownershipValidator,
37
+ prefixEnforcer,
38
+ converter
39
+ );
40
+ });
41
+
42
+ describe('createTemplate', () => {
43
+ it('should create template successfully', async () => {
44
+ const template: Template = {
45
+ id: 'local-123',
46
+ name: 'Welcome',
47
+ subject: 'Welcome!',
48
+ htmlContent: '<html>Test</html>',
49
+ category: 'transactional',
50
+ tags: [],
51
+ version: '1.0.0',
52
+ isActive: true,
53
+ requiredVariables: [],
54
+ createdAt: new Date(),
55
+ updatedAt: new Date(),
56
+ createdBy: 'test',
57
+ lastModifiedBy: 'test'
58
+ };
59
+
60
+ mockClient.request.mockResolvedValueOnce({
61
+ statusCode: 201,
62
+ headers: {},
63
+ body: JSON.stringify({ id: 'sendgrid-123', name: 'Test - Welcome', generation: 'dynamic', updated_at: new Date().toISOString() })
64
+ });
65
+
66
+ mockClient.isSuccessResponse.mockReturnValue(true);
67
+ mockClient.parseResponseBody.mockReturnValueOnce({ id: 'sendgrid-123', name: 'Test - Welcome', generation: 'dynamic', updated_at: new Date().toISOString() });
68
+
69
+ mockClient.request.mockResolvedValueOnce({
70
+ statusCode: 201,
71
+ headers: {},
72
+ body: JSON.stringify({ id: 'version-123' })
73
+ });
74
+
75
+ mockClient.isSuccessResponse.mockReturnValue(true);
76
+ mockClient.parseResponseBody.mockReturnValueOnce({ id: 'version-123' });
77
+
78
+ const result = await manager.createTemplate(template);
79
+
80
+ expect(result).toBe('sendgrid-123');
81
+ expect(mockClient.request).toHaveBeenCalledTimes(2);
82
+ });
83
+ });
84
+
85
+ describe('listTemplates', () => {
86
+ it('should list and register templates', async () => {
87
+ mockClient.request.mockResolvedValueOnce({
88
+ statusCode: 200,
89
+ headers: {},
90
+ body: JSON.stringify({
91
+ result: [
92
+ { id: 'template-1', name: 'Test - Template 1', generation: 'dynamic', updated_at: new Date().toISOString(), versions: [] }
93
+ ]
94
+ })
95
+ });
96
+
97
+ mockClient.isSuccessResponse.mockReturnValue(true);
98
+ mockClient.parseResponseBody.mockReturnValueOnce({
99
+ result: [
100
+ { id: 'template-1', name: 'Test - Template 1', generation: 'dynamic', updated_at: new Date().toISOString(), versions: [] }
101
+ ]
102
+ });
103
+
104
+ const result = await manager.listTemplates();
105
+
106
+ expect(result).toHaveLength(1);
107
+ expect(ownershipValidator.isOwnedTemplate('template-1')).toBe(true);
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,130 @@
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 { OwnershipValidator } from '../../src/safety/ownership-validator';
10
+ import { UnauthorizedTemplateAccessError } from '../../src/api/errors';
11
+
12
+ describe('OwnershipValidator', () => {
13
+ describe('constructor', () => {
14
+ it('should create validator with specified safety mode', () => {
15
+ const validator = new OwnershipValidator('strict');
16
+ expect(validator).toBeDefined();
17
+ });
18
+ });
19
+
20
+ describe('registerTemplate', () => {
21
+ it('should register template successfully', () => {
22
+ const validator = new OwnershipValidator('strict');
23
+
24
+ validator.registerTemplate('template-123');
25
+
26
+ expect(validator.isOwnedTemplate('template-123')).toBe(true);
27
+ });
28
+
29
+ it('should not add duplicate registrations', () => {
30
+ const validator = new OwnershipValidator('strict');
31
+
32
+ validator.registerTemplate('template-123');
33
+ validator.registerTemplate('template-123');
34
+
35
+ expect(validator.getOwnedTemplates()).toEqual(['template-123']);
36
+ });
37
+ });
38
+
39
+ describe('unregisterTemplate', () => {
40
+ it('should unregister template successfully', () => {
41
+ const validator = new OwnershipValidator('strict');
42
+
43
+ validator.registerTemplate('template-123');
44
+ expect(validator.isOwnedTemplate('template-123')).toBe(true);
45
+
46
+ validator.unregisterTemplate('template-123');
47
+ expect(validator.isOwnedTemplate('template-123')).toBe(false);
48
+ });
49
+
50
+ it('should handle unregistering non-existent template', () => {
51
+ const validator = new OwnershipValidator('strict');
52
+
53
+ expect(() => validator.unregisterTemplate('non-existent')).not.toThrow();
54
+ });
55
+ });
56
+
57
+ describe('validateOwnership', () => {
58
+ it('should pass validation for owned template in strict mode', () => {
59
+ const validator = new OwnershipValidator('strict');
60
+ validator.registerTemplate('template-123');
61
+
62
+ expect(() => validator.validateOwnership('template-123')).not.toThrow();
63
+ });
64
+
65
+ it('should throw error for unowned template in strict mode', () => {
66
+ const validator = new OwnershipValidator('strict');
67
+
68
+ expect(() => validator.validateOwnership('template-123')).toThrow(UnauthorizedTemplateAccessError);
69
+ });
70
+
71
+ it('should pass validation for any template in permissive mode', () => {
72
+ const validator = new OwnershipValidator('permissive');
73
+
74
+ expect(() => validator.validateOwnership('template-123')).not.toThrow();
75
+ expect(() => validator.validateOwnership('unregistered')).not.toThrow();
76
+ });
77
+ });
78
+
79
+ describe('isOwnedTemplate', () => {
80
+ it('should return true for registered templates', () => {
81
+ const validator = new OwnershipValidator('strict');
82
+ validator.registerTemplate('template-123');
83
+
84
+ expect(validator.isOwnedTemplate('template-123')).toBe(true);
85
+ });
86
+
87
+ it('should return false for unregistered templates', () => {
88
+ const validator = new OwnershipValidator('strict');
89
+
90
+ expect(validator.isOwnedTemplate('template-123')).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('getOwnedTemplates', () => {
95
+ it('should return all registered templates', () => {
96
+ const validator = new OwnershipValidator('strict');
97
+
98
+ validator.registerTemplate('template-1');
99
+ validator.registerTemplate('template-2');
100
+ validator.registerTemplate('template-3');
101
+
102
+ const owned = validator.getOwnedTemplates();
103
+ expect(owned).toHaveLength(3);
104
+ expect(owned).toContain('template-1');
105
+ expect(owned).toContain('template-2');
106
+ expect(owned).toContain('template-3');
107
+ });
108
+
109
+ it('should return empty array when no templates registered', () => {
110
+ const validator = new OwnershipValidator('strict');
111
+
112
+ expect(validator.getOwnedTemplates()).toEqual([]);
113
+ });
114
+ });
115
+
116
+ describe('clearRegistrations', () => {
117
+ it('should clear all registered templates', () => {
118
+ const validator = new OwnershipValidator('strict');
119
+
120
+ validator.registerTemplate('template-1');
121
+ validator.registerTemplate('template-2');
122
+
123
+ expect(validator.getOwnedTemplates()).toHaveLength(2);
124
+
125
+ validator.clearRegistrations();
126
+
127
+ expect(validator.getOwnedTemplates()).toEqual([]);
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,136 @@
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 { PrefixEnforcer } from '../../src/safety/prefix-enforcer';
10
+
11
+ describe('PrefixEnforcer', () => {
12
+ describe('constructor', () => {
13
+ it('should create enforcer with specified prefix', () => {
14
+ const enforcer = new PrefixEnforcer('MyApp - ');
15
+ expect(enforcer.getPrefix()).toBe('MyApp - ');
16
+ });
17
+ });
18
+
19
+ describe('applyPrefix', () => {
20
+ it('should add prefix to name without prefix', () => {
21
+ const enforcer = new PrefixEnforcer('MyApp - ');
22
+
23
+ const result = enforcer.applyPrefix('Welcome Email');
24
+
25
+ expect(result).toBe('MyApp - Welcome Email');
26
+ });
27
+
28
+ it('should not add prefix if already present', () => {
29
+ const enforcer = new PrefixEnforcer('MyApp - ');
30
+
31
+ const result = enforcer.applyPrefix('MyApp - Welcome Email');
32
+
33
+ expect(result).toBe('MyApp - Welcome Email');
34
+ });
35
+
36
+ it('should handle empty string', () => {
37
+ const enforcer = new PrefixEnforcer('MyApp - ');
38
+
39
+ const result = enforcer.applyPrefix('');
40
+
41
+ expect(result).toBe('MyApp - ');
42
+ });
43
+
44
+ it('should work with different prefix formats', () => {
45
+ const enforcer = new PrefixEnforcer('[PROD]');
46
+
47
+ expect(enforcer.applyPrefix('template')).toBe('[PROD]template');
48
+ expect(enforcer.applyPrefix('[PROD]template')).toBe('[PROD]template');
49
+ });
50
+ });
51
+
52
+ describe('stripPrefix', () => {
53
+ it('should remove prefix from name', () => {
54
+ const enforcer = new PrefixEnforcer('MyApp - ');
55
+
56
+ const result = enforcer.stripPrefix('MyApp - Welcome Email');
57
+
58
+ expect(result).toBe('Welcome Email');
59
+ });
60
+
61
+ it('should return name unchanged if no prefix', () => {
62
+ const enforcer = new PrefixEnforcer('MyApp - ');
63
+
64
+ const result = enforcer.stripPrefix('Welcome Email');
65
+
66
+ expect(result).toBe('Welcome Email');
67
+ });
68
+
69
+ it('should handle empty string', () => {
70
+ const enforcer = new PrefixEnforcer('MyApp - ');
71
+
72
+ const result = enforcer.stripPrefix('');
73
+
74
+ expect(result).toBe('');
75
+ });
76
+
77
+ it('should only strip the prefix once', () => {
78
+ const enforcer = new PrefixEnforcer('App - ');
79
+
80
+ const result = enforcer.stripPrefix('App - App - Template');
81
+
82
+ expect(result).toBe('App - Template');
83
+ });
84
+ });
85
+
86
+ describe('hasPrefix', () => {
87
+ it('should return true for names with prefix', () => {
88
+ const enforcer = new PrefixEnforcer('MyApp - ');
89
+
90
+ expect(enforcer.hasPrefix('MyApp - Welcome')).toBe(true);
91
+ });
92
+
93
+ it('should return false for names without prefix', () => {
94
+ const enforcer = new PrefixEnforcer('MyApp - ');
95
+
96
+ expect(enforcer.hasPrefix('Welcome')).toBe(false);
97
+ });
98
+
99
+ it('should return false for empty string', () => {
100
+ const enforcer = new PrefixEnforcer('MyApp - ');
101
+
102
+ expect(enforcer.hasPrefix('')).toBe(false);
103
+ });
104
+
105
+ it('should be case-sensitive', () => {
106
+ const enforcer = new PrefixEnforcer('MyApp - ');
107
+
108
+ expect(enforcer.hasPrefix('myapp - Welcome')).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe('getPrefix', () => {
113
+ it('should return the configured prefix', () => {
114
+ const enforcer = new PrefixEnforcer('Test - ');
115
+
116
+ expect(enforcer.getPrefix()).toBe('Test - ');
117
+ });
118
+ });
119
+
120
+ describe('edge cases', () => {
121
+ it('should handle prefix-only names', () => {
122
+ const enforcer = new PrefixEnforcer('Prefix');
123
+
124
+ expect(enforcer.applyPrefix('Prefix')).toBe('Prefix');
125
+ expect(enforcer.stripPrefix('Prefix')).toBe('');
126
+ expect(enforcer.hasPrefix('Prefix')).toBe(true);
127
+ });
128
+
129
+ it('should handle special characters in prefix', () => {
130
+ const enforcer = new PrefixEnforcer('[App-v1.0]_');
131
+
132
+ expect(enforcer.applyPrefix('template')).toBe('[App-v1.0]_template');
133
+ expect(enforcer.stripPrefix('[App-v1.0]_template')).toBe('template');
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,20 @@
1
+ export declare class SendGridApiError extends Error {
2
+ statusCode?: number | undefined;
3
+ response?: unknown | undefined;
4
+ constructor(message: string, statusCode?: number | undefined, response?: unknown | undefined);
5
+ }
6
+ export declare class TemplateNotFoundError extends Error {
7
+ templateId: string;
8
+ constructor(templateId: string);
9
+ }
10
+ export declare class UnauthorizedTemplateAccessError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ export declare class TemplateConflictError extends Error {
14
+ templateId: string;
15
+ conflicts: string[];
16
+ constructor(templateId: string, conflicts: string[]);
17
+ }
18
+ export declare class RateLimitError extends Error {
19
+ constructor(message: string);
20
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RateLimitError = exports.TemplateConflictError = exports.UnauthorizedTemplateAccessError = exports.TemplateNotFoundError = exports.SendGridApiError = void 0;
11
+ class SendGridApiError extends Error {
12
+ constructor(message, statusCode, response) {
13
+ super(message);
14
+ this.statusCode = statusCode;
15
+ this.response = response;
16
+ this.name = 'SendGridApiError';
17
+ }
18
+ }
19
+ exports.SendGridApiError = SendGridApiError;
20
+ class TemplateNotFoundError extends Error {
21
+ constructor(templateId) {
22
+ super(`Template not found: ${templateId}`);
23
+ this.templateId = templateId;
24
+ this.name = 'TemplateNotFoundError';
25
+ }
26
+ }
27
+ exports.TemplateNotFoundError = TemplateNotFoundError;
28
+ class UnauthorizedTemplateAccessError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'UnauthorizedTemplateAccessError';
32
+ }
33
+ }
34
+ exports.UnauthorizedTemplateAccessError = UnauthorizedTemplateAccessError;
35
+ class TemplateConflictError extends Error {
36
+ constructor(templateId, conflicts) {
37
+ super(`Template conflicts detected: ${conflicts.join(', ')}`);
38
+ this.templateId = templateId;
39
+ this.conflicts = conflicts;
40
+ this.name = 'TemplateConflictError';
41
+ }
42
+ }
43
+ exports.TemplateConflictError = TemplateConflictError;
44
+ class RateLimitError extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'RateLimitError';
48
+ }
49
+ }
50
+ exports.RateLimitError = RateLimitError;
@@ -0,0 +1,17 @@
1
+ export interface RateLimiterConfig {
2
+ maxRequestsPerSecond: number;
3
+ maxRequestsPerMinute: number;
4
+ }
5
+ export declare class SimpleRateLimiter {
6
+ private requestTimestamps;
7
+ private readonly maxRequestsPerSecond;
8
+ private readonly maxRequestsPerMinute;
9
+ constructor(config: RateLimiterConfig);
10
+ acquire(): Promise<void>;
11
+ release(): void;
12
+ private sleep;
13
+ getRemainingCapacity(): {
14
+ perSecond: number;
15
+ perMinute: number;
16
+ };
17
+ }