@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.
- package/.eslintrc.js +27 -0
- package/README.md +432 -0
- package/__tests__/api/rate-limiter.test.ts +125 -0
- package/__tests__/converters/sender-converter.test.ts +148 -0
- package/__tests__/converters/template-converter.test.ts +230 -0
- package/__tests__/plugin/sender-manager.test.ts +135 -0
- package/__tests__/plugin/sendgrid-plugin.test.ts +195 -0
- package/__tests__/plugin/template-manager.test.ts +110 -0
- package/__tests__/safety/ownership-validator.test.ts +130 -0
- package/__tests__/safety/prefix-enforcer.test.ts +136 -0
- package/dist/api/errors.d.ts +20 -0
- package/dist/api/errors.js +50 -0
- package/dist/api/rate-limiter.d.ts +17 -0
- package/dist/api/rate-limiter.js +58 -0
- package/dist/api/sendgrid-client.d.ts +21 -0
- package/dist/api/sendgrid-client.js +67 -0
- package/dist/converters/sender-converter.d.ts +6 -0
- package/dist/converters/sender-converter.js +40 -0
- package/dist/converters/template-converter.d.ts +11 -0
- package/dist/converters/template-converter.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +42 -0
- package/dist/plugin/sender-manager.d.ts +20 -0
- package/dist/plugin/sender-manager.js +124 -0
- package/dist/plugin/sendgrid-plugin.d.ts +60 -0
- package/dist/plugin/sendgrid-plugin.js +204 -0
- package/dist/plugin/sync-manager.d.ts +19 -0
- package/dist/plugin/sync-manager.js +94 -0
- package/dist/plugin/template-manager.d.ts +25 -0
- package/dist/plugin/template-manager.js +152 -0
- package/dist/safety/ownership-validator.d.ts +15 -0
- package/dist/safety/ownership-validator.js +42 -0
- package/dist/safety/prefix-enforcer.d.ts +8 -0
- package/dist/safety/prefix-enforcer.js +34 -0
- package/dist/types/config-types.d.ts +24 -0
- package/dist/types/config-types.js +9 -0
- package/dist/types/plugin-types.d.ts +79 -0
- package/dist/types/plugin-types.js +9 -0
- package/dist/types/sendgrid-types.d.ts +56 -0
- package/dist/types/sendgrid-types.js +9 -0
- package/jest.config.js +25 -0
- package/package.json +42 -0
- package/src/api/errors.ts +49 -0
- package/src/api/rate-limiter.ts +79 -0
- package/src/api/sendgrid-client.ts +86 -0
- package/src/converters/sender-converter.ts +41 -0
- package/src/converters/template-converter.ts +97 -0
- package/src/index.ts +58 -0
- package/src/plugin/sender-manager.ts +166 -0
- package/src/plugin/sendgrid-plugin.ts +316 -0
- package/src/plugin/sync-manager.ts +115 -0
- package/src/plugin/template-manager.ts +240 -0
- package/src/safety/ownership-validator.ts +55 -0
- package/src/safety/prefix-enforcer.ts +37 -0
- package/src/types/config-types.ts +34 -0
- package/src/types/plugin-types.ts +97 -0
- package/src/types/sendgrid-types.ts +69 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,240 @@
|
|
|
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 { SendGridClient } from '../api/sendgrid-client';
|
|
10
|
+
import { SendGridApiError, TemplateNotFoundError, TemplateConflictError } from '../api/errors';
|
|
11
|
+
import { OwnershipValidator } from '../safety/ownership-validator';
|
|
12
|
+
import { PrefixEnforcer } from '../safety/prefix-enforcer';
|
|
13
|
+
import { TemplateConverter } from '../converters/template-converter';
|
|
14
|
+
import { Template, ProviderTemplate, TemplateVersion } from '../types/plugin-types';
|
|
15
|
+
import {
|
|
16
|
+
SendGridTemplateData,
|
|
17
|
+
SendGridTemplateVersion
|
|
18
|
+
} from '../types/sendgrid-types';
|
|
19
|
+
|
|
20
|
+
export class TemplateManager {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly client: SendGridClient,
|
|
23
|
+
private readonly ownershipValidator: OwnershipValidator,
|
|
24
|
+
private readonly prefixEnforcer: PrefixEnforcer,
|
|
25
|
+
private readonly converter: TemplateConverter,
|
|
26
|
+
private readonly logger?: {
|
|
27
|
+
info: (message: string) => void;
|
|
28
|
+
error: (message: string) => void;
|
|
29
|
+
}
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async createTemplate(template: Template): Promise<string> {
|
|
33
|
+
// Apply template naming convention
|
|
34
|
+
const templateName = this.prefixEnforcer.applyPrefix(template.name);
|
|
35
|
+
|
|
36
|
+
// Create base template in SendGrid
|
|
37
|
+
const createRequest = {
|
|
38
|
+
name: templateName,
|
|
39
|
+
generation: 'dynamic'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const response = await this.client.request('/templates', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: JSON.stringify(createRequest)
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
48
|
+
throw new SendGridApiError(
|
|
49
|
+
`Failed to create template: ${response.statusCode}`,
|
|
50
|
+
response.statusCode,
|
|
51
|
+
response.body
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const templateData = this.client.parseResponseBody<SendGridTemplateData>(response);
|
|
56
|
+
const templateId = templateData.id;
|
|
57
|
+
|
|
58
|
+
// Register as app-managed
|
|
59
|
+
this.ownershipValidator.registerTemplate(templateId);
|
|
60
|
+
|
|
61
|
+
// Create initial version with content
|
|
62
|
+
await this.createTemplateVersion(templateId, {
|
|
63
|
+
name: 'v1.0.0',
|
|
64
|
+
subject: template.subject,
|
|
65
|
+
htmlContent: template.htmlContent,
|
|
66
|
+
textContent: template.textContent,
|
|
67
|
+
active: true
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Log operation
|
|
71
|
+
this.logger?.info(`Created SendGrid template: ${templateName} (ID: ${templateId})`);
|
|
72
|
+
|
|
73
|
+
return templateId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async updateTemplate(providerTemplateId: string, template: Template): Promise<void> {
|
|
77
|
+
// Validate template ownership
|
|
78
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
79
|
+
|
|
80
|
+
// Get current template state
|
|
81
|
+
const currentTemplate = await this.getTemplate(providerTemplateId);
|
|
82
|
+
|
|
83
|
+
// Check for conflicts
|
|
84
|
+
if (this.hasConflicts(currentTemplate, template)) {
|
|
85
|
+
throw new TemplateConflictError(
|
|
86
|
+
providerTemplateId,
|
|
87
|
+
this.detectConflicts(currentTemplate, template)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create new version with updated content
|
|
92
|
+
await this.createTemplateVersion(providerTemplateId, {
|
|
93
|
+
name: `v${template.version}`,
|
|
94
|
+
subject: template.subject,
|
|
95
|
+
htmlContent: template.htmlContent,
|
|
96
|
+
textContent: template.textContent,
|
|
97
|
+
active: template.isActive
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.logger?.info(`Updated SendGrid template: ${providerTemplateId}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async deleteTemplate(providerTemplateId: string): Promise<void> {
|
|
104
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
105
|
+
|
|
106
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
107
|
+
method: 'DELETE'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
111
|
+
throw new SendGridApiError(
|
|
112
|
+
`Failed to delete template: ${response.statusCode}`,
|
|
113
|
+
response.statusCode,
|
|
114
|
+
response.body
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.ownershipValidator.unregisterTemplate(providerTemplateId);
|
|
119
|
+
this.logger?.info(`Deleted SendGrid template: ${providerTemplateId}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getTemplate(providerTemplateId: string): Promise<ProviderTemplate> {
|
|
123
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
124
|
+
|
|
125
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
126
|
+
method: 'GET'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (response.statusCode === 404) {
|
|
130
|
+
throw new TemplateNotFoundError(providerTemplateId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
134
|
+
throw new SendGridApiError(
|
|
135
|
+
`Failed to get template: ${response.statusCode}`,
|
|
136
|
+
response.statusCode,
|
|
137
|
+
response.body
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const templateData = this.client.parseResponseBody<SendGridTemplateData>(response);
|
|
142
|
+
return this.converter.convertToProviderTemplate(templateData);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async listTemplates(): Promise<ProviderTemplate[]> {
|
|
146
|
+
const response = await this.client.request('/templates?generations=dynamic', {
|
|
147
|
+
method: 'GET'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
151
|
+
throw new SendGridApiError(
|
|
152
|
+
`Failed to list templates: ${response.statusCode}`,
|
|
153
|
+
response.statusCode,
|
|
154
|
+
response.body
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const data = this.client.parseResponseBody<{ result: SendGridTemplateData[] }>(response);
|
|
159
|
+
|
|
160
|
+
// Filter to only app-managed templates
|
|
161
|
+
const appTemplates = data.result.filter(template =>
|
|
162
|
+
this.prefixEnforcer.hasPrefix(template.name)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Register all app templates
|
|
166
|
+
appTemplates.forEach(template => {
|
|
167
|
+
this.ownershipValidator.registerTemplate(template.id);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return appTemplates.map(template =>
|
|
171
|
+
this.converter.convertToProviderTemplate(template)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async createTemplateVersion(
|
|
176
|
+
templateId: string,
|
|
177
|
+
version: TemplateVersion
|
|
178
|
+
): Promise<string> {
|
|
179
|
+
const versionRequest = {
|
|
180
|
+
template_id: templateId,
|
|
181
|
+
name: version.name,
|
|
182
|
+
subject: version.subject,
|
|
183
|
+
html_content: version.htmlContent,
|
|
184
|
+
plain_content: version.textContent || '',
|
|
185
|
+
active: version.active ? 1 : 0
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const response = await this.client.request(`/templates/${templateId}/versions`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
body: JSON.stringify(versionRequest)
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
194
|
+
throw new SendGridApiError(
|
|
195
|
+
`Failed to create template version: ${response.statusCode}`,
|
|
196
|
+
response.statusCode,
|
|
197
|
+
response.body
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const versionData = this.client.parseResponseBody<SendGridTemplateVersion>(response);
|
|
202
|
+
return versionData.id;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async activateTemplateVersion(templateId: string, versionId: string): Promise<void> {
|
|
206
|
+
const response = await this.client.request(
|
|
207
|
+
`/templates/${templateId}/versions/${versionId}/activate`,
|
|
208
|
+
{ method: 'POST' }
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
212
|
+
throw new SendGridApiError(
|
|
213
|
+
`Failed to activate template version: ${response.statusCode}`,
|
|
214
|
+
response.statusCode,
|
|
215
|
+
response.body
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.logger?.info(`Activated template version: ${templateId}/${versionId}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private hasConflicts(current: ProviderTemplate, updated: Template): boolean {
|
|
223
|
+
// Simple conflict detection - can be enhanced
|
|
224
|
+
return current.updatedAt > updated.updatedAt;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private detectConflicts(current: ProviderTemplate, updated: Template): string[] {
|
|
228
|
+
const conflicts: string[] = [];
|
|
229
|
+
|
|
230
|
+
if (current.subject !== updated.subject) {
|
|
231
|
+
conflicts.push('subject modified');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (current.htmlContent !== updated.htmlContent) {
|
|
235
|
+
conflicts.push('HTML content modified');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return conflicts;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { UnauthorizedTemplateAccessError } from '../api/errors';
|
|
10
|
+
|
|
11
|
+
export type SafetyMode = 'strict' | 'permissive';
|
|
12
|
+
|
|
13
|
+
export class OwnershipValidator {
|
|
14
|
+
private appTemplateIds: Set<string> = new Set();
|
|
15
|
+
private readonly safetyMode: SafetyMode;
|
|
16
|
+
private readonly logger?: {
|
|
17
|
+
debug: (message: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
constructor(safetyMode: SafetyMode, logger?: { debug: (message: string) => void }) {
|
|
21
|
+
this.safetyMode = safetyMode;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
registerTemplate(templateId: string): void {
|
|
26
|
+
this.appTemplateIds.add(templateId);
|
|
27
|
+
this.logger?.debug(`Registered app template: ${templateId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
unregisterTemplate(templateId: string): void {
|
|
31
|
+
this.appTemplateIds.delete(templateId);
|
|
32
|
+
this.logger?.debug(`Unregistered app template: ${templateId}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
validateOwnership(templateId: string): void {
|
|
36
|
+
if (this.safetyMode === 'strict' && !this.appTemplateIds.has(templateId)) {
|
|
37
|
+
throw new UnauthorizedTemplateAccessError(
|
|
38
|
+
`Template ${templateId} is not managed by this application`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isOwnedTemplate(templateId: string): boolean {
|
|
44
|
+
return this.appTemplateIds.has(templateId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getOwnedTemplates(): string[] {
|
|
48
|
+
return Array.from(this.appTemplateIds);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
clearRegistrations(): void {
|
|
52
|
+
this.appTemplateIds.clear();
|
|
53
|
+
this.logger?.debug('Cleared all template registrations');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export class PrefixEnforcer {
|
|
10
|
+
private readonly prefix: string;
|
|
11
|
+
|
|
12
|
+
constructor(prefix: string) {
|
|
13
|
+
this.prefix = prefix;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
applyPrefix(name: string): string {
|
|
17
|
+
if (name.startsWith(this.prefix)) {
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
return `${this.prefix}${name}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
stripPrefix(name: string): string {
|
|
24
|
+
if (name.startsWith(this.prefix)) {
|
|
25
|
+
return name.substring(this.prefix.length);
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
hasPrefix(name: string): boolean {
|
|
31
|
+
return name.startsWith(this.prefix);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getPrefix(): string {
|
|
35
|
+
return this.prefix;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export interface SendGridPluginConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
templatePrefix: string;
|
|
12
|
+
sharedAccount?: boolean;
|
|
13
|
+
safetyMode: 'strict' | 'permissive';
|
|
14
|
+
rateLimits?: {
|
|
15
|
+
templatesPerMinute: number;
|
|
16
|
+
apiCallsPerSecond: number;
|
|
17
|
+
};
|
|
18
|
+
webhookConfig?: {
|
|
19
|
+
secret: string;
|
|
20
|
+
eventTypes: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ApiRequestOptions {
|
|
25
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
body?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ApiResponse {
|
|
31
|
+
statusCode: number;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
body: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
export interface PluginConfig {
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProviderCapability {
|
|
14
|
+
feature: string;
|
|
15
|
+
description: string;
|
|
16
|
+
methods: string[];
|
|
17
|
+
config: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Template {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
subject: string;
|
|
25
|
+
htmlContent: string;
|
|
26
|
+
textContent?: string;
|
|
27
|
+
category: string;
|
|
28
|
+
tags: string[];
|
|
29
|
+
version: string;
|
|
30
|
+
isActive: boolean;
|
|
31
|
+
context?: string;
|
|
32
|
+
variables?: Record<string, unknown>;
|
|
33
|
+
requiredVariables: string[];
|
|
34
|
+
providerTemplateIds?: Record<string, string>;
|
|
35
|
+
lastSyncedAt?: Record<string, Date>;
|
|
36
|
+
syncStatus?: Record<string, string>;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
createdBy: string;
|
|
40
|
+
lastModifiedBy: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProviderTemplate {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
subject: string;
|
|
48
|
+
htmlContent: string;
|
|
49
|
+
textContent?: string;
|
|
50
|
+
version: string;
|
|
51
|
+
active: boolean;
|
|
52
|
+
createdAt: Date;
|
|
53
|
+
updatedAt: Date;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TemplateVersion {
|
|
57
|
+
name: string;
|
|
58
|
+
subject: string;
|
|
59
|
+
htmlContent: string;
|
|
60
|
+
textContent?: string;
|
|
61
|
+
active: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SyncResult {
|
|
65
|
+
success: boolean;
|
|
66
|
+
action: 'created' | 'updated' | 'deleted' | 'failed';
|
|
67
|
+
providerTemplateId?: string;
|
|
68
|
+
localTemplateId?: string;
|
|
69
|
+
error?: string;
|
|
70
|
+
metadata: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ValidationResult {
|
|
74
|
+
isValid: boolean;
|
|
75
|
+
errors: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RouteConflict {
|
|
79
|
+
templateId: string;
|
|
80
|
+
conflicts: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DateRange {
|
|
84
|
+
start: Date;
|
|
85
|
+
end: Date;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TemplateAnalytics {
|
|
89
|
+
templateId: string;
|
|
90
|
+
opens: number;
|
|
91
|
+
clicks: number;
|
|
92
|
+
bounces: number;
|
|
93
|
+
deliveries: number;
|
|
94
|
+
dateRange: DateRange;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FeatureConfig = Record<string, unknown>;
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
export interface VerifiedSender {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name: string;
|
|
13
|
+
isVerified: boolean;
|
|
14
|
+
domain: string;
|
|
15
|
+
verificationStatus: 'pending' | 'verified' | 'failed';
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
lastVerifiedAt?: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SenderValidationResult {
|
|
21
|
+
isValid: boolean;
|
|
22
|
+
verifiedSender?: VerifiedSender;
|
|
23
|
+
domain: string;
|
|
24
|
+
domainMatches: VerifiedSender[];
|
|
25
|
+
errors: string[];
|
|
26
|
+
suggestions: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SendGridTemplateData {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
generation: 'legacy' | 'dynamic';
|
|
33
|
+
updated_at: string;
|
|
34
|
+
versions?: SendGridTemplateVersion[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SendGridTemplateVersion {
|
|
38
|
+
id: string;
|
|
39
|
+
template_id: string;
|
|
40
|
+
active: number;
|
|
41
|
+
name: string;
|
|
42
|
+
html_content: string;
|
|
43
|
+
plain_content?: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
updated_at: string;
|
|
46
|
+
editor: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SendGridVerifiedSenderData {
|
|
50
|
+
id: number;
|
|
51
|
+
nickname: string;
|
|
52
|
+
from_email: string;
|
|
53
|
+
from_name: string;
|
|
54
|
+
reply_to: string;
|
|
55
|
+
reply_to_name: string;
|
|
56
|
+
address: string;
|
|
57
|
+
address2: string;
|
|
58
|
+
city: string;
|
|
59
|
+
state: string;
|
|
60
|
+
zip: string;
|
|
61
|
+
country: string;
|
|
62
|
+
verified: boolean;
|
|
63
|
+
locked: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SendGridApiError extends Error {
|
|
67
|
+
statusCode?: number;
|
|
68
|
+
response?: unknown;
|
|
69
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"moduleResolution": "node"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "__tests__"]
|
|
18
|
+
}
|