@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,204 @@
|
|
|
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.SendGridTemplatePlugin = void 0;
|
|
11
|
+
const sendgrid_client_1 = require("../api/sendgrid-client");
|
|
12
|
+
const rate_limiter_1 = require("../api/rate-limiter");
|
|
13
|
+
const ownership_validator_1 = require("../safety/ownership-validator");
|
|
14
|
+
const prefix_enforcer_1 = require("../safety/prefix-enforcer");
|
|
15
|
+
const template_converter_1 = require("../converters/template-converter");
|
|
16
|
+
const sender_converter_1 = require("../converters/sender-converter");
|
|
17
|
+
const template_manager_1 = require("./template-manager");
|
|
18
|
+
const sender_manager_1 = require("./sender-manager");
|
|
19
|
+
const sync_manager_1 = require("./sync-manager");
|
|
20
|
+
class SendGridTemplatePlugin {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.name = 'sendgrid-templates';
|
|
23
|
+
this.version = '1.0.0';
|
|
24
|
+
this.providerId = 'sendgrid';
|
|
25
|
+
this.initialized = false;
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
async initialize(__pluginConfig) {
|
|
29
|
+
if (this.initialized) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Initialize rate limiter if configured
|
|
33
|
+
if (this.config.rateLimits) {
|
|
34
|
+
this.rateLimiter = new rate_limiter_1.SimpleRateLimiter({
|
|
35
|
+
maxRequestsPerSecond: this.config.rateLimits.apiCallsPerSecond,
|
|
36
|
+
maxRequestsPerMinute: this.config.rateLimits.templatesPerMinute
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Initialize SendGrid client
|
|
40
|
+
this.client = new sendgrid_client_1.SendGridClient({
|
|
41
|
+
apiKey: this.config.apiKey,
|
|
42
|
+
rateLimiter: this.rateLimiter,
|
|
43
|
+
logger: this.logger
|
|
44
|
+
});
|
|
45
|
+
// Initialize safety components
|
|
46
|
+
this.ownershipValidator = new ownership_validator_1.OwnershipValidator(this.config.safetyMode, this.logger);
|
|
47
|
+
this.prefixEnforcer = new prefix_enforcer_1.PrefixEnforcer(this.config.templatePrefix);
|
|
48
|
+
// Initialize converters
|
|
49
|
+
this.templateConverter = new template_converter_1.TemplateConverter(this.prefixEnforcer);
|
|
50
|
+
this.senderConverter = new sender_converter_1.SenderConverter();
|
|
51
|
+
// Initialize managers
|
|
52
|
+
this.templateManager = new template_manager_1.TemplateManager(this.client, this.ownershipValidator, this.prefixEnforcer, this.templateConverter, this.logger);
|
|
53
|
+
this.senderManager = new sender_manager_1.SenderManager(this.client, this.senderConverter, this.logger);
|
|
54
|
+
this.syncManager = new sync_manager_1.SyncManager(this.templateManager, this.templateConverter, this.client, this.logger);
|
|
55
|
+
// Load existing templates
|
|
56
|
+
await this.loadExistingTemplates();
|
|
57
|
+
this.initialized = true;
|
|
58
|
+
this.logger?.info('SendGrid plugin initialized successfully');
|
|
59
|
+
}
|
|
60
|
+
async destroy() {
|
|
61
|
+
if (!this.initialized) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.ownershipValidator.clearRegistrations();
|
|
65
|
+
this.senderManager.clearCache();
|
|
66
|
+
this.initialized = false;
|
|
67
|
+
this.logger?.info('SendGrid plugin destroyed');
|
|
68
|
+
}
|
|
69
|
+
// Template operations
|
|
70
|
+
async createTemplate(template) {
|
|
71
|
+
this.ensureInitialized();
|
|
72
|
+
return this.templateManager.createTemplate(template);
|
|
73
|
+
}
|
|
74
|
+
async updateTemplate(providerTemplateId, template) {
|
|
75
|
+
this.ensureInitialized();
|
|
76
|
+
return this.templateManager.updateTemplate(providerTemplateId, template);
|
|
77
|
+
}
|
|
78
|
+
async deleteTemplate(providerTemplateId) {
|
|
79
|
+
this.ensureInitialized();
|
|
80
|
+
return this.templateManager.deleteTemplate(providerTemplateId);
|
|
81
|
+
}
|
|
82
|
+
async getTemplate(providerTemplateId) {
|
|
83
|
+
this.ensureInitialized();
|
|
84
|
+
return this.templateManager.getTemplate(providerTemplateId);
|
|
85
|
+
}
|
|
86
|
+
async listTemplates() {
|
|
87
|
+
this.ensureInitialized();
|
|
88
|
+
return this.templateManager.listTemplates();
|
|
89
|
+
}
|
|
90
|
+
// Synchronization
|
|
91
|
+
async syncToProvider(template) {
|
|
92
|
+
this.ensureInitialized();
|
|
93
|
+
return this.syncManager.syncToProvider(template);
|
|
94
|
+
}
|
|
95
|
+
async syncFromProvider(providerTemplateId) {
|
|
96
|
+
this.ensureInitialized();
|
|
97
|
+
return this.syncManager.syncFromProvider(providerTemplateId);
|
|
98
|
+
}
|
|
99
|
+
// SendGrid-specific features
|
|
100
|
+
async getVerifiedSenders() {
|
|
101
|
+
this.ensureInitialized();
|
|
102
|
+
return this.senderManager.getVerifiedSenders();
|
|
103
|
+
}
|
|
104
|
+
async validateSender(email) {
|
|
105
|
+
this.ensureInitialized();
|
|
106
|
+
return this.senderManager.validateSender(email);
|
|
107
|
+
}
|
|
108
|
+
async selectBestSender(email) {
|
|
109
|
+
this.ensureInitialized();
|
|
110
|
+
return this.senderManager.selectBestSender(email);
|
|
111
|
+
}
|
|
112
|
+
async createTemplateVersion(templateId, version) {
|
|
113
|
+
this.ensureInitialized();
|
|
114
|
+
return this.templateManager.createTemplateVersion(templateId, version);
|
|
115
|
+
}
|
|
116
|
+
async activateTemplateVersion(templateId, versionId) {
|
|
117
|
+
this.ensureInitialized();
|
|
118
|
+
return this.templateManager.activateTemplateVersion(templateId, versionId);
|
|
119
|
+
}
|
|
120
|
+
async getTemplateAnalytics(__templateId, __dateRange) {
|
|
121
|
+
this.ensureInitialized();
|
|
122
|
+
// TODO: Implement analytics fetching from SendGrid
|
|
123
|
+
throw new Error('Template analytics not yet implemented');
|
|
124
|
+
}
|
|
125
|
+
async batchSyncTemplates(templates) {
|
|
126
|
+
this.ensureInitialized();
|
|
127
|
+
return this.syncManager.batchSync(templates);
|
|
128
|
+
}
|
|
129
|
+
// Capabilities
|
|
130
|
+
getCapabilities() {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
feature: 'template-management',
|
|
134
|
+
description: 'Full CRUD operations for SendGrid templates',
|
|
135
|
+
methods: ['createTemplate', 'updateTemplate', 'deleteTemplate', 'getTemplate', 'listTemplates'],
|
|
136
|
+
config: {
|
|
137
|
+
supportsVersioning: true,
|
|
138
|
+
supportsActivation: true,
|
|
139
|
+
maxNameLength: 100,
|
|
140
|
+
templatePrefix: this.config.templatePrefix
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
feature: 'sender-verification',
|
|
145
|
+
description: 'SendGrid verified sender management',
|
|
146
|
+
methods: ['getVerifiedSenders', 'validateSender', 'selectBestSender'],
|
|
147
|
+
config: {
|
|
148
|
+
supportsDomainMatching: true,
|
|
149
|
+
supportsAutoSelection: true
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
feature: 'template-analytics',
|
|
154
|
+
description: 'SendGrid template usage and performance analytics',
|
|
155
|
+
methods: ['getTemplateAnalytics'],
|
|
156
|
+
config: {
|
|
157
|
+
supportsRealTime: false,
|
|
158
|
+
historicalDataDays: 30,
|
|
159
|
+
metricsAvailable: ['opens', 'clicks', 'bounces', 'deliveries']
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
feature: 'batch-operations',
|
|
164
|
+
description: 'Efficient bulk template operations',
|
|
165
|
+
methods: ['batchSyncTemplates'],
|
|
166
|
+
config: {
|
|
167
|
+
maxBatchSize: 50,
|
|
168
|
+
supportsTransactions: false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
supportsFeature(feature) {
|
|
174
|
+
return this.getCapabilities().some(cap => cap.feature === feature);
|
|
175
|
+
}
|
|
176
|
+
getFeatureConfig(feature) {
|
|
177
|
+
const capability = this.getCapabilities().find(cap => cap.feature === feature);
|
|
178
|
+
return capability?.config || null;
|
|
179
|
+
}
|
|
180
|
+
validateRoute(__route) {
|
|
181
|
+
// Placeholder validation
|
|
182
|
+
return {
|
|
183
|
+
isValid: true,
|
|
184
|
+
errors: []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Private methods
|
|
188
|
+
async loadExistingTemplates() {
|
|
189
|
+
try {
|
|
190
|
+
const templates = await this.templateManager.listTemplates();
|
|
191
|
+
this.logger?.info(`Loaded ${templates.length} existing SendGrid templates`);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
195
|
+
this.logger?.error(`Failed to load existing templates: ${errorMessage}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
ensureInitialized() {
|
|
199
|
+
if (!this.initialized) {
|
|
200
|
+
throw new Error('SendGrid plugin not initialized. Call initialize() first.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
exports.SendGridTemplatePlugin = SendGridTemplatePlugin;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TemplateManager } from './template-manager';
|
|
2
|
+
import { TemplateConverter } from '../converters/template-converter';
|
|
3
|
+
import { SendGridClient } from '../api/sendgrid-client';
|
|
4
|
+
import { Template, SyncResult } from '../types/plugin-types';
|
|
5
|
+
export declare class SyncManager {
|
|
6
|
+
private readonly templateManager;
|
|
7
|
+
private readonly converter;
|
|
8
|
+
private readonly client;
|
|
9
|
+
private readonly logger?;
|
|
10
|
+
constructor(templateManager: TemplateManager, converter: TemplateConverter, client: SendGridClient, logger?: {
|
|
11
|
+
info: (message: string) => void;
|
|
12
|
+
error: (message: string) => void;
|
|
13
|
+
} | undefined);
|
|
14
|
+
syncToProvider(template: Template): Promise<SyncResult>;
|
|
15
|
+
syncFromProvider(providerTemplateId: string): Promise<Template>;
|
|
16
|
+
batchSync(templates: Template[]): Promise<SyncResult[]>;
|
|
17
|
+
private generateLocalId;
|
|
18
|
+
private sleep;
|
|
19
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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.SyncManager = void 0;
|
|
11
|
+
class SyncManager {
|
|
12
|
+
constructor(templateManager, converter, client, logger) {
|
|
13
|
+
this.templateManager = templateManager;
|
|
14
|
+
this.converter = converter;
|
|
15
|
+
this.client = client;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
async syncToProvider(template) {
|
|
19
|
+
try {
|
|
20
|
+
const existingProviderId = template.providerTemplateIds?.sendgrid;
|
|
21
|
+
if (existingProviderId) {
|
|
22
|
+
// Update existing template
|
|
23
|
+
await this.templateManager.updateTemplate(existingProviderId, template);
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
action: 'updated',
|
|
27
|
+
providerTemplateId: existingProviderId,
|
|
28
|
+
localTemplateId: template.id,
|
|
29
|
+
metadata: {
|
|
30
|
+
newVersion: template.version
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Create new template
|
|
36
|
+
const newProviderId = await this.templateManager.createTemplate(template);
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
action: 'created',
|
|
40
|
+
providerTemplateId: newProviderId,
|
|
41
|
+
localTemplateId: template.id,
|
|
42
|
+
metadata: {
|
|
43
|
+
version: template.version
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
50
|
+
this.logger?.error(`Template sync failed: ${errorMessage}`);
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
action: 'failed',
|
|
54
|
+
localTemplateId: template.id,
|
|
55
|
+
error: errorMessage,
|
|
56
|
+
metadata: {
|
|
57
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
58
|
+
timestamp: new Date()
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async syncFromProvider(providerTemplateId) {
|
|
64
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
65
|
+
method: 'GET'
|
|
66
|
+
});
|
|
67
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
68
|
+
throw new Error(`Failed to fetch template from SendGrid: ${response.statusCode}`);
|
|
69
|
+
}
|
|
70
|
+
const templateData = this.client.parseResponseBody(response);
|
|
71
|
+
const localId = this.generateLocalId();
|
|
72
|
+
return this.converter.convertToLocalTemplate(templateData, localId);
|
|
73
|
+
}
|
|
74
|
+
async batchSync(templates) {
|
|
75
|
+
this.logger?.info(`Starting batch sync of ${templates.length} templates`);
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const template of templates) {
|
|
78
|
+
const result = await this.syncToProvider(template);
|
|
79
|
+
results.push(result);
|
|
80
|
+
// Add delay to respect rate limits
|
|
81
|
+
await this.sleep(100);
|
|
82
|
+
}
|
|
83
|
+
const successful = results.filter(r => r.success).length;
|
|
84
|
+
this.logger?.info(`Batch sync completed: ${successful}/${templates.length} successful`);
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
generateLocalId() {
|
|
88
|
+
return `local_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
89
|
+
}
|
|
90
|
+
sleep(ms) {
|
|
91
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.SyncManager = SyncManager;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SendGridClient } from '../api/sendgrid-client';
|
|
2
|
+
import { OwnershipValidator } from '../safety/ownership-validator';
|
|
3
|
+
import { PrefixEnforcer } from '../safety/prefix-enforcer';
|
|
4
|
+
import { TemplateConverter } from '../converters/template-converter';
|
|
5
|
+
import { Template, ProviderTemplate, TemplateVersion } from '../types/plugin-types';
|
|
6
|
+
export declare class TemplateManager {
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly ownershipValidator;
|
|
9
|
+
private readonly prefixEnforcer;
|
|
10
|
+
private readonly converter;
|
|
11
|
+
private readonly logger?;
|
|
12
|
+
constructor(client: SendGridClient, ownershipValidator: OwnershipValidator, prefixEnforcer: PrefixEnforcer, converter: TemplateConverter, logger?: {
|
|
13
|
+
info: (message: string) => void;
|
|
14
|
+
error: (message: string) => void;
|
|
15
|
+
} | undefined);
|
|
16
|
+
createTemplate(template: Template): Promise<string>;
|
|
17
|
+
updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
|
|
18
|
+
deleteTemplate(providerTemplateId: string): Promise<void>;
|
|
19
|
+
getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
|
|
20
|
+
listTemplates(): Promise<ProviderTemplate[]>;
|
|
21
|
+
createTemplateVersion(templateId: string, version: TemplateVersion): Promise<string>;
|
|
22
|
+
activateTemplateVersion(templateId: string, versionId: string): Promise<void>;
|
|
23
|
+
private hasConflicts;
|
|
24
|
+
private detectConflicts;
|
|
25
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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.TemplateManager = void 0;
|
|
11
|
+
const errors_1 = require("../api/errors");
|
|
12
|
+
class TemplateManager {
|
|
13
|
+
constructor(client, ownershipValidator, prefixEnforcer, converter, logger) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.ownershipValidator = ownershipValidator;
|
|
16
|
+
this.prefixEnforcer = prefixEnforcer;
|
|
17
|
+
this.converter = converter;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
}
|
|
20
|
+
async createTemplate(template) {
|
|
21
|
+
// Apply template naming convention
|
|
22
|
+
const templateName = this.prefixEnforcer.applyPrefix(template.name);
|
|
23
|
+
// Create base template in SendGrid
|
|
24
|
+
const createRequest = {
|
|
25
|
+
name: templateName,
|
|
26
|
+
generation: 'dynamic'
|
|
27
|
+
};
|
|
28
|
+
const response = await this.client.request('/templates', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: JSON.stringify(createRequest)
|
|
31
|
+
});
|
|
32
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
33
|
+
throw new errors_1.SendGridApiError(`Failed to create template: ${response.statusCode}`, response.statusCode, response.body);
|
|
34
|
+
}
|
|
35
|
+
const templateData = this.client.parseResponseBody(response);
|
|
36
|
+
const templateId = templateData.id;
|
|
37
|
+
// Register as app-managed
|
|
38
|
+
this.ownershipValidator.registerTemplate(templateId);
|
|
39
|
+
// Create initial version with content
|
|
40
|
+
await this.createTemplateVersion(templateId, {
|
|
41
|
+
name: 'v1.0.0',
|
|
42
|
+
subject: template.subject,
|
|
43
|
+
htmlContent: template.htmlContent,
|
|
44
|
+
textContent: template.textContent,
|
|
45
|
+
active: true
|
|
46
|
+
});
|
|
47
|
+
// Log operation
|
|
48
|
+
this.logger?.info(`Created SendGrid template: ${templateName} (ID: ${templateId})`);
|
|
49
|
+
return templateId;
|
|
50
|
+
}
|
|
51
|
+
async updateTemplate(providerTemplateId, template) {
|
|
52
|
+
// Validate template ownership
|
|
53
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
54
|
+
// Get current template state
|
|
55
|
+
const currentTemplate = await this.getTemplate(providerTemplateId);
|
|
56
|
+
// Check for conflicts
|
|
57
|
+
if (this.hasConflicts(currentTemplate, template)) {
|
|
58
|
+
throw new errors_1.TemplateConflictError(providerTemplateId, this.detectConflicts(currentTemplate, template));
|
|
59
|
+
}
|
|
60
|
+
// Create new version with updated content
|
|
61
|
+
await this.createTemplateVersion(providerTemplateId, {
|
|
62
|
+
name: `v${template.version}`,
|
|
63
|
+
subject: template.subject,
|
|
64
|
+
htmlContent: template.htmlContent,
|
|
65
|
+
textContent: template.textContent,
|
|
66
|
+
active: template.isActive
|
|
67
|
+
});
|
|
68
|
+
this.logger?.info(`Updated SendGrid template: ${providerTemplateId}`);
|
|
69
|
+
}
|
|
70
|
+
async deleteTemplate(providerTemplateId) {
|
|
71
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
72
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
73
|
+
method: 'DELETE'
|
|
74
|
+
});
|
|
75
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
76
|
+
throw new errors_1.SendGridApiError(`Failed to delete template: ${response.statusCode}`, response.statusCode, response.body);
|
|
77
|
+
}
|
|
78
|
+
this.ownershipValidator.unregisterTemplate(providerTemplateId);
|
|
79
|
+
this.logger?.info(`Deleted SendGrid template: ${providerTemplateId}`);
|
|
80
|
+
}
|
|
81
|
+
async getTemplate(providerTemplateId) {
|
|
82
|
+
this.ownershipValidator.validateOwnership(providerTemplateId);
|
|
83
|
+
const response = await this.client.request(`/templates/${providerTemplateId}`, {
|
|
84
|
+
method: 'GET'
|
|
85
|
+
});
|
|
86
|
+
if (response.statusCode === 404) {
|
|
87
|
+
throw new errors_1.TemplateNotFoundError(providerTemplateId);
|
|
88
|
+
}
|
|
89
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
90
|
+
throw new errors_1.SendGridApiError(`Failed to get template: ${response.statusCode}`, response.statusCode, response.body);
|
|
91
|
+
}
|
|
92
|
+
const templateData = this.client.parseResponseBody(response);
|
|
93
|
+
return this.converter.convertToProviderTemplate(templateData);
|
|
94
|
+
}
|
|
95
|
+
async listTemplates() {
|
|
96
|
+
const response = await this.client.request('/templates?generations=dynamic', {
|
|
97
|
+
method: 'GET'
|
|
98
|
+
});
|
|
99
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
100
|
+
throw new errors_1.SendGridApiError(`Failed to list templates: ${response.statusCode}`, response.statusCode, response.body);
|
|
101
|
+
}
|
|
102
|
+
const data = this.client.parseResponseBody(response);
|
|
103
|
+
// Filter to only app-managed templates
|
|
104
|
+
const appTemplates = data.result.filter(template => this.prefixEnforcer.hasPrefix(template.name));
|
|
105
|
+
// Register all app templates
|
|
106
|
+
appTemplates.forEach(template => {
|
|
107
|
+
this.ownershipValidator.registerTemplate(template.id);
|
|
108
|
+
});
|
|
109
|
+
return appTemplates.map(template => this.converter.convertToProviderTemplate(template));
|
|
110
|
+
}
|
|
111
|
+
async createTemplateVersion(templateId, version) {
|
|
112
|
+
const versionRequest = {
|
|
113
|
+
template_id: templateId,
|
|
114
|
+
name: version.name,
|
|
115
|
+
subject: version.subject,
|
|
116
|
+
html_content: version.htmlContent,
|
|
117
|
+
plain_content: version.textContent || '',
|
|
118
|
+
active: version.active ? 1 : 0
|
|
119
|
+
};
|
|
120
|
+
const response = await this.client.request(`/templates/${templateId}/versions`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify(versionRequest)
|
|
123
|
+
});
|
|
124
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
125
|
+
throw new errors_1.SendGridApiError(`Failed to create template version: ${response.statusCode}`, response.statusCode, response.body);
|
|
126
|
+
}
|
|
127
|
+
const versionData = this.client.parseResponseBody(response);
|
|
128
|
+
return versionData.id;
|
|
129
|
+
}
|
|
130
|
+
async activateTemplateVersion(templateId, versionId) {
|
|
131
|
+
const response = await this.client.request(`/templates/${templateId}/versions/${versionId}/activate`, { method: 'POST' });
|
|
132
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
133
|
+
throw new errors_1.SendGridApiError(`Failed to activate template version: ${response.statusCode}`, response.statusCode, response.body);
|
|
134
|
+
}
|
|
135
|
+
this.logger?.info(`Activated template version: ${templateId}/${versionId}`);
|
|
136
|
+
}
|
|
137
|
+
hasConflicts(current, updated) {
|
|
138
|
+
// Simple conflict detection - can be enhanced
|
|
139
|
+
return current.updatedAt > updated.updatedAt;
|
|
140
|
+
}
|
|
141
|
+
detectConflicts(current, updated) {
|
|
142
|
+
const conflicts = [];
|
|
143
|
+
if (current.subject !== updated.subject) {
|
|
144
|
+
conflicts.push('subject modified');
|
|
145
|
+
}
|
|
146
|
+
if (current.htmlContent !== updated.htmlContent) {
|
|
147
|
+
conflicts.push('HTML content modified');
|
|
148
|
+
}
|
|
149
|
+
return conflicts;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.TemplateManager = TemplateManager;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SafetyMode = 'strict' | 'permissive';
|
|
2
|
+
export declare class OwnershipValidator {
|
|
3
|
+
private appTemplateIds;
|
|
4
|
+
private readonly safetyMode;
|
|
5
|
+
private readonly logger?;
|
|
6
|
+
constructor(safetyMode: SafetyMode, logger?: {
|
|
7
|
+
debug: (message: string) => void;
|
|
8
|
+
});
|
|
9
|
+
registerTemplate(templateId: string): void;
|
|
10
|
+
unregisterTemplate(templateId: string): void;
|
|
11
|
+
validateOwnership(templateId: string): void;
|
|
12
|
+
isOwnedTemplate(templateId: string): boolean;
|
|
13
|
+
getOwnedTemplates(): string[];
|
|
14
|
+
clearRegistrations(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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.OwnershipValidator = void 0;
|
|
11
|
+
const errors_1 = require("../api/errors");
|
|
12
|
+
class OwnershipValidator {
|
|
13
|
+
constructor(safetyMode, logger) {
|
|
14
|
+
this.appTemplateIds = new Set();
|
|
15
|
+
this.safetyMode = safetyMode;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
registerTemplate(templateId) {
|
|
19
|
+
this.appTemplateIds.add(templateId);
|
|
20
|
+
this.logger?.debug(`Registered app template: ${templateId}`);
|
|
21
|
+
}
|
|
22
|
+
unregisterTemplate(templateId) {
|
|
23
|
+
this.appTemplateIds.delete(templateId);
|
|
24
|
+
this.logger?.debug(`Unregistered app template: ${templateId}`);
|
|
25
|
+
}
|
|
26
|
+
validateOwnership(templateId) {
|
|
27
|
+
if (this.safetyMode === 'strict' && !this.appTemplateIds.has(templateId)) {
|
|
28
|
+
throw new errors_1.UnauthorizedTemplateAccessError(`Template ${templateId} is not managed by this application`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
isOwnedTemplate(templateId) {
|
|
32
|
+
return this.appTemplateIds.has(templateId);
|
|
33
|
+
}
|
|
34
|
+
getOwnedTemplates() {
|
|
35
|
+
return Array.from(this.appTemplateIds);
|
|
36
|
+
}
|
|
37
|
+
clearRegistrations() {
|
|
38
|
+
this.appTemplateIds.clear();
|
|
39
|
+
this.logger?.debug('Cleared all template registrations');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.OwnershipValidator = OwnershipValidator;
|
|
@@ -0,0 +1,34 @@
|
|
|
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.PrefixEnforcer = void 0;
|
|
11
|
+
class PrefixEnforcer {
|
|
12
|
+
constructor(prefix) {
|
|
13
|
+
this.prefix = prefix;
|
|
14
|
+
}
|
|
15
|
+
applyPrefix(name) {
|
|
16
|
+
if (name.startsWith(this.prefix)) {
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
19
|
+
return `${this.prefix}${name}`;
|
|
20
|
+
}
|
|
21
|
+
stripPrefix(name) {
|
|
22
|
+
if (name.startsWith(this.prefix)) {
|
|
23
|
+
return name.substring(this.prefix.length);
|
|
24
|
+
}
|
|
25
|
+
return name;
|
|
26
|
+
}
|
|
27
|
+
hasPrefix(name) {
|
|
28
|
+
return name.startsWith(this.prefix);
|
|
29
|
+
}
|
|
30
|
+
getPrefix() {
|
|
31
|
+
return this.prefix;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.PrefixEnforcer = PrefixEnforcer;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SendGridPluginConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
templatePrefix: string;
|
|
4
|
+
sharedAccount?: boolean;
|
|
5
|
+
safetyMode: 'strict' | 'permissive';
|
|
6
|
+
rateLimits?: {
|
|
7
|
+
templatesPerMinute: number;
|
|
8
|
+
apiCallsPerSecond: number;
|
|
9
|
+
};
|
|
10
|
+
webhookConfig?: {
|
|
11
|
+
secret: string;
|
|
12
|
+
eventTypes: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ApiRequestOptions {
|
|
16
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
body?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ApiResponse {
|
|
21
|
+
statusCode: number;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
body: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 });
|