@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,58 @@
|
|
|
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.SimpleRateLimiter = void 0;
|
|
11
|
+
const errors_1 = require("./errors");
|
|
12
|
+
class SimpleRateLimiter {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.requestTimestamps = [];
|
|
15
|
+
this.maxRequestsPerSecond = config.maxRequestsPerSecond;
|
|
16
|
+
this.maxRequestsPerMinute = config.maxRequestsPerMinute;
|
|
17
|
+
}
|
|
18
|
+
async acquire() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
// Clean up old timestamps
|
|
21
|
+
this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < 60000 // Keep last minute
|
|
22
|
+
);
|
|
23
|
+
// Check per-second limit
|
|
24
|
+
const lastSecond = this.requestTimestamps.filter(timestamp => now - timestamp < 1000);
|
|
25
|
+
if (lastSecond.length >= this.maxRequestsPerSecond) {
|
|
26
|
+
const waitTime = 1000 - (now - lastSecond[0]);
|
|
27
|
+
await this.sleep(waitTime);
|
|
28
|
+
return this.acquire();
|
|
29
|
+
}
|
|
30
|
+
// Check per-minute limit
|
|
31
|
+
if (this.requestTimestamps.length >= this.maxRequestsPerMinute) {
|
|
32
|
+
const waitTime = 60000 - (now - this.requestTimestamps[0]);
|
|
33
|
+
if (waitTime > 5000) {
|
|
34
|
+
throw new errors_1.RateLimitError(`Rate limit exceeded, would need to wait ${Math.ceil(waitTime / 1000)}s`);
|
|
35
|
+
}
|
|
36
|
+
await this.sleep(waitTime);
|
|
37
|
+
return this.acquire();
|
|
38
|
+
}
|
|
39
|
+
// Add timestamp and proceed
|
|
40
|
+
this.requestTimestamps.push(now);
|
|
41
|
+
}
|
|
42
|
+
release() {
|
|
43
|
+
// No-op for simple implementation
|
|
44
|
+
}
|
|
45
|
+
sleep(ms) {
|
|
46
|
+
return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));
|
|
47
|
+
}
|
|
48
|
+
getRemainingCapacity() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const lastSecond = this.requestTimestamps.filter(timestamp => now - timestamp < 1000).length;
|
|
51
|
+
const lastMinute = this.requestTimestamps.length;
|
|
52
|
+
return {
|
|
53
|
+
perSecond: Math.max(0, this.maxRequestsPerSecond - lastSecond),
|
|
54
|
+
perMinute: Math.max(0, this.maxRequestsPerMinute - lastMinute)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.SimpleRateLimiter = SimpleRateLimiter;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ApiRequestOptions, ApiResponse } from '../types/config-types';
|
|
2
|
+
import { SimpleRateLimiter } from './rate-limiter';
|
|
3
|
+
export interface SendGridClientConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
rateLimiter?: SimpleRateLimiter;
|
|
6
|
+
logger?: {
|
|
7
|
+
debug: (message: string) => void;
|
|
8
|
+
error: (message: string) => void;
|
|
9
|
+
info: (message: string) => void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare class SendGridClient {
|
|
13
|
+
private readonly apiKey;
|
|
14
|
+
private readonly rateLimiter?;
|
|
15
|
+
private readonly logger?;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
constructor(config: SendGridClientConfig);
|
|
18
|
+
request(endpoint: string, options: ApiRequestOptions): Promise<ApiResponse>;
|
|
19
|
+
isSuccessResponse(response: ApiResponse): boolean;
|
|
20
|
+
parseResponseBody<T>(response: ApiResponse): T;
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.SendGridClient = void 0;
|
|
11
|
+
const errors_1 = require("./errors");
|
|
12
|
+
class SendGridClient {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.baseUrl = 'https://api.sendgrid.com/v3';
|
|
15
|
+
this.apiKey = config.apiKey;
|
|
16
|
+
this.rateLimiter = config.rateLimiter;
|
|
17
|
+
this.logger = config.logger;
|
|
18
|
+
}
|
|
19
|
+
async request(endpoint, options) {
|
|
20
|
+
// Apply rate limiting if configured
|
|
21
|
+
if (this.rateLimiter) {
|
|
22
|
+
await this.rateLimiter.acquire();
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: options.method,
|
|
28
|
+
headers: {
|
|
29
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
...options.headers
|
|
32
|
+
},
|
|
33
|
+
body: options.body
|
|
34
|
+
});
|
|
35
|
+
const body = await response.text();
|
|
36
|
+
// Log all API calls for audit trail
|
|
37
|
+
this.logger?.debug(`SendGrid API ${options.method} ${endpoint}: ${response.status}`);
|
|
38
|
+
return {
|
|
39
|
+
statusCode: response.status,
|
|
40
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
41
|
+
body
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
46
|
+
this.logger?.error(`SendGrid API error: ${errorMessage}`);
|
|
47
|
+
throw new errors_1.SendGridApiError(`API request failed: ${errorMessage}`, undefined, error);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (this.rateLimiter) {
|
|
51
|
+
this.rateLimiter.release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
isSuccessResponse(response) {
|
|
56
|
+
return response.statusCode >= 200 && response.statusCode < 300;
|
|
57
|
+
}
|
|
58
|
+
parseResponseBody(response) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(response.body);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new errors_1.SendGridApiError('Failed to parse response body', response.statusCode, response.body);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.SendGridClient = SendGridClient;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { VerifiedSender, SendGridVerifiedSenderData } from '../types/sendgrid-types';
|
|
2
|
+
export declare class SenderConverter {
|
|
3
|
+
convertToVerifiedSender(sendgridData: SendGridVerifiedSenderData): VerifiedSender;
|
|
4
|
+
extractDomain(email: string): string;
|
|
5
|
+
isDomainMatch(domain1: string, domain2: string): boolean;
|
|
6
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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.SenderConverter = void 0;
|
|
11
|
+
class SenderConverter {
|
|
12
|
+
convertToVerifiedSender(sendgridData) {
|
|
13
|
+
const domain = this.extractDomain(sendgridData.from_email);
|
|
14
|
+
return {
|
|
15
|
+
id: sendgridData.id.toString(),
|
|
16
|
+
email: sendgridData.from_email,
|
|
17
|
+
name: sendgridData.from_name || sendgridData.nickname,
|
|
18
|
+
isVerified: sendgridData.verified,
|
|
19
|
+
domain,
|
|
20
|
+
verificationStatus: sendgridData.verified ? 'verified' : 'pending',
|
|
21
|
+
createdAt: new Date(), // SendGrid doesn't provide creation date
|
|
22
|
+
lastVerifiedAt: sendgridData.verified ? new Date() : undefined
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
extractDomain(email) {
|
|
26
|
+
return email.split('@')[1]?.toLowerCase() || '';
|
|
27
|
+
}
|
|
28
|
+
isDomainMatch(domain1, domain2) {
|
|
29
|
+
// Exact match
|
|
30
|
+
if (domain1 === domain2)
|
|
31
|
+
return true;
|
|
32
|
+
// Subdomain match (e.g., app.example.com matches example.com)
|
|
33
|
+
if (domain1.endsWith(`.${domain2}`))
|
|
34
|
+
return true;
|
|
35
|
+
if (domain2.endsWith(`.${domain1}`))
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.SenderConverter = SenderConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SendGridTemplateData, SendGridTemplateVersion } from '../types/sendgrid-types';
|
|
2
|
+
import { ProviderTemplate, Template } from '../types/plugin-types';
|
|
3
|
+
import { PrefixEnforcer } from '../safety/prefix-enforcer';
|
|
4
|
+
export declare class TemplateConverter {
|
|
5
|
+
private readonly prefixEnforcer;
|
|
6
|
+
constructor(prefixEnforcer: PrefixEnforcer);
|
|
7
|
+
convertToProviderTemplate(sendgridData: SendGridTemplateData, activeVersion?: SendGridTemplateVersion): ProviderTemplate;
|
|
8
|
+
convertToLocalTemplate(sendgridData: SendGridTemplateData, localId: string, activeVersion?: SendGridTemplateVersion): Template;
|
|
9
|
+
extractVariables(content: string): string[];
|
|
10
|
+
private findActiveVersion;
|
|
11
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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.TemplateConverter = void 0;
|
|
11
|
+
class TemplateConverter {
|
|
12
|
+
constructor(prefixEnforcer) {
|
|
13
|
+
this.prefixEnforcer = prefixEnforcer;
|
|
14
|
+
}
|
|
15
|
+
convertToProviderTemplate(sendgridData, activeVersion) {
|
|
16
|
+
// Find active version if not provided
|
|
17
|
+
const version = activeVersion || this.findActiveVersion(sendgridData.versions || []);
|
|
18
|
+
return {
|
|
19
|
+
id: sendgridData.id,
|
|
20
|
+
name: this.prefixEnforcer.stripPrefix(sendgridData.name),
|
|
21
|
+
subject: version?.subject || '',
|
|
22
|
+
htmlContent: version?.html_content || '',
|
|
23
|
+
textContent: version?.plain_content,
|
|
24
|
+
version: version?.name || '1.0.0',
|
|
25
|
+
active: version?.active === 1,
|
|
26
|
+
createdAt: new Date(sendgridData.updated_at),
|
|
27
|
+
updatedAt: new Date(sendgridData.updated_at)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
convertToLocalTemplate(sendgridData, localId, activeVersion) {
|
|
31
|
+
const providerTemplate = this.convertToProviderTemplate(sendgridData, activeVersion);
|
|
32
|
+
return {
|
|
33
|
+
id: localId,
|
|
34
|
+
name: providerTemplate.name,
|
|
35
|
+
subject: providerTemplate.subject,
|
|
36
|
+
htmlContent: providerTemplate.htmlContent,
|
|
37
|
+
textContent: providerTemplate.textContent,
|
|
38
|
+
category: 'synced',
|
|
39
|
+
tags: ['sendgrid', 'synced'],
|
|
40
|
+
version: providerTemplate.version,
|
|
41
|
+
isActive: providerTemplate.active,
|
|
42
|
+
requiredVariables: [],
|
|
43
|
+
providerTemplateIds: {
|
|
44
|
+
sendgrid: sendgridData.id
|
|
45
|
+
},
|
|
46
|
+
lastSyncedAt: {
|
|
47
|
+
sendgrid: new Date()
|
|
48
|
+
},
|
|
49
|
+
syncStatus: {
|
|
50
|
+
sendgrid: 'success'
|
|
51
|
+
},
|
|
52
|
+
createdAt: providerTemplate.createdAt,
|
|
53
|
+
updatedAt: providerTemplate.updatedAt,
|
|
54
|
+
createdBy: 'sendgrid-sync',
|
|
55
|
+
lastModifiedBy: 'sendgrid-sync'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
extractVariables(content) {
|
|
59
|
+
// Extract Handlebars-style variables: {{variableName}}
|
|
60
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
61
|
+
const matches = content.matchAll(regex);
|
|
62
|
+
const variables = new Set();
|
|
63
|
+
for (const match of matches) {
|
|
64
|
+
const variable = match[1].trim();
|
|
65
|
+
// Filter out Handlebars helpers
|
|
66
|
+
if (!variable.startsWith('#') && !variable.startsWith('/')) {
|
|
67
|
+
variables.add(variable);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Array.from(variables);
|
|
71
|
+
}
|
|
72
|
+
findActiveVersion(versions) {
|
|
73
|
+
return versions.find(v => v.active === 1) || versions[0];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.TemplateConverter = TemplateConverter;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { SendGridTemplatePlugin, TemplateProviderPlugin } from './plugin/sendgrid-plugin';
|
|
2
|
+
export { TemplateManager } from './plugin/template-manager';
|
|
3
|
+
export { SenderManager } from './plugin/sender-manager';
|
|
4
|
+
export { SyncManager } from './plugin/sync-manager';
|
|
5
|
+
export { SendGridClient } from './api/sendgrid-client';
|
|
6
|
+
export { SimpleRateLimiter } from './api/rate-limiter';
|
|
7
|
+
export { OwnershipValidator, SafetyMode } from './safety/ownership-validator';
|
|
8
|
+
export { PrefixEnforcer } from './safety/prefix-enforcer';
|
|
9
|
+
export { TemplateConverter } from './converters/template-converter';
|
|
10
|
+
export { SenderConverter } from './converters/sender-converter';
|
|
11
|
+
export { SendGridApiError, TemplateNotFoundError, UnauthorizedTemplateAccessError, TemplateConflictError, RateLimitError } from './api/errors';
|
|
12
|
+
export type { SendGridPluginConfig, ApiRequestOptions, ApiResponse } from './types/config-types';
|
|
13
|
+
export type { PluginConfig, ProviderCapability, Template, ProviderTemplate, TemplateVersion, SyncResult, ValidationResult, DateRange, TemplateAnalytics, FeatureConfig } from './types/plugin-types';
|
|
14
|
+
export type { VerifiedSender, SenderValidationResult, SendGridTemplateData, SendGridTemplateVersion, SendGridVerifiedSenderData } from './types/sendgrid-types';
|
package/dist/index.js
ADDED
|
@@ -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.RateLimitError = exports.TemplateConflictError = exports.UnauthorizedTemplateAccessError = exports.TemplateNotFoundError = exports.SendGridApiError = exports.SenderConverter = exports.TemplateConverter = exports.PrefixEnforcer = exports.OwnershipValidator = exports.SimpleRateLimiter = exports.SendGridClient = exports.SyncManager = exports.SenderManager = exports.TemplateManager = exports.SendGridTemplatePlugin = void 0;
|
|
11
|
+
// Main plugin class
|
|
12
|
+
var sendgrid_plugin_1 = require("./plugin/sendgrid-plugin");
|
|
13
|
+
Object.defineProperty(exports, "SendGridTemplatePlugin", { enumerable: true, get: function () { return sendgrid_plugin_1.SendGridTemplatePlugin; } });
|
|
14
|
+
// Plugin components
|
|
15
|
+
var template_manager_1 = require("./plugin/template-manager");
|
|
16
|
+
Object.defineProperty(exports, "TemplateManager", { enumerable: true, get: function () { return template_manager_1.TemplateManager; } });
|
|
17
|
+
var sender_manager_1 = require("./plugin/sender-manager");
|
|
18
|
+
Object.defineProperty(exports, "SenderManager", { enumerable: true, get: function () { return sender_manager_1.SenderManager; } });
|
|
19
|
+
var sync_manager_1 = require("./plugin/sync-manager");
|
|
20
|
+
Object.defineProperty(exports, "SyncManager", { enumerable: true, get: function () { return sync_manager_1.SyncManager; } });
|
|
21
|
+
// API components
|
|
22
|
+
var sendgrid_client_1 = require("./api/sendgrid-client");
|
|
23
|
+
Object.defineProperty(exports, "SendGridClient", { enumerable: true, get: function () { return sendgrid_client_1.SendGridClient; } });
|
|
24
|
+
var rate_limiter_1 = require("./api/rate-limiter");
|
|
25
|
+
Object.defineProperty(exports, "SimpleRateLimiter", { enumerable: true, get: function () { return rate_limiter_1.SimpleRateLimiter; } });
|
|
26
|
+
// Safety components
|
|
27
|
+
var ownership_validator_1 = require("./safety/ownership-validator");
|
|
28
|
+
Object.defineProperty(exports, "OwnershipValidator", { enumerable: true, get: function () { return ownership_validator_1.OwnershipValidator; } });
|
|
29
|
+
var prefix_enforcer_1 = require("./safety/prefix-enforcer");
|
|
30
|
+
Object.defineProperty(exports, "PrefixEnforcer", { enumerable: true, get: function () { return prefix_enforcer_1.PrefixEnforcer; } });
|
|
31
|
+
// Converters
|
|
32
|
+
var template_converter_1 = require("./converters/template-converter");
|
|
33
|
+
Object.defineProperty(exports, "TemplateConverter", { enumerable: true, get: function () { return template_converter_1.TemplateConverter; } });
|
|
34
|
+
var sender_converter_1 = require("./converters/sender-converter");
|
|
35
|
+
Object.defineProperty(exports, "SenderConverter", { enumerable: true, get: function () { return sender_converter_1.SenderConverter; } });
|
|
36
|
+
// Errors
|
|
37
|
+
var errors_1 = require("./api/errors");
|
|
38
|
+
Object.defineProperty(exports, "SendGridApiError", { enumerable: true, get: function () { return errors_1.SendGridApiError; } });
|
|
39
|
+
Object.defineProperty(exports, "TemplateNotFoundError", { enumerable: true, get: function () { return errors_1.TemplateNotFoundError; } });
|
|
40
|
+
Object.defineProperty(exports, "UnauthorizedTemplateAccessError", { enumerable: true, get: function () { return errors_1.UnauthorizedTemplateAccessError; } });
|
|
41
|
+
Object.defineProperty(exports, "TemplateConflictError", { enumerable: true, get: function () { return errors_1.TemplateConflictError; } });
|
|
42
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SendGridClient } from '../api/sendgrid-client';
|
|
2
|
+
import { SenderConverter } from '../converters/sender-converter';
|
|
3
|
+
import { VerifiedSender, SenderValidationResult } from '../types/sendgrid-types';
|
|
4
|
+
export declare class SenderManager {
|
|
5
|
+
private readonly client;
|
|
6
|
+
private readonly converter;
|
|
7
|
+
private readonly logger?;
|
|
8
|
+
private sendersCache?;
|
|
9
|
+
private cacheTimestamp?;
|
|
10
|
+
private readonly cacheTTL;
|
|
11
|
+
constructor(client: SendGridClient, converter: SenderConverter, logger?: {
|
|
12
|
+
info: (message: string) => void;
|
|
13
|
+
debug: (message: string) => void;
|
|
14
|
+
} | undefined);
|
|
15
|
+
getVerifiedSenders(useCache?: boolean): Promise<VerifiedSender[]>;
|
|
16
|
+
validateSender(email: string): Promise<SenderValidationResult>;
|
|
17
|
+
selectBestSender(fromEmail: string): Promise<VerifiedSender | null>;
|
|
18
|
+
clearCache(): void;
|
|
19
|
+
private getSenderSuggestions;
|
|
20
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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.SenderManager = void 0;
|
|
11
|
+
const errors_1 = require("../api/errors");
|
|
12
|
+
class SenderManager {
|
|
13
|
+
constructor(client, converter, logger) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.converter = converter;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
this.cacheTTL = 300000; // 5 minutes
|
|
18
|
+
}
|
|
19
|
+
async getVerifiedSenders(useCache = true) {
|
|
20
|
+
// Return cached data if valid
|
|
21
|
+
if (useCache &&
|
|
22
|
+
this.sendersCache &&
|
|
23
|
+
this.cacheTimestamp &&
|
|
24
|
+
Date.now() - this.cacheTimestamp < this.cacheTTL) {
|
|
25
|
+
this.logger?.debug('Returning cached verified senders');
|
|
26
|
+
return this.sendersCache;
|
|
27
|
+
}
|
|
28
|
+
const response = await this.client.request('/verified_senders', {
|
|
29
|
+
method: 'GET'
|
|
30
|
+
});
|
|
31
|
+
if (!this.client.isSuccessResponse(response)) {
|
|
32
|
+
throw new errors_1.SendGridApiError('Failed to fetch verified senders', response.statusCode, response.body);
|
|
33
|
+
}
|
|
34
|
+
const data = this.client.parseResponseBody(response);
|
|
35
|
+
const senders = data.results.map(sender => this.converter.convertToVerifiedSender(sender));
|
|
36
|
+
// Update cache
|
|
37
|
+
this.sendersCache = senders;
|
|
38
|
+
this.cacheTimestamp = Date.now();
|
|
39
|
+
this.logger?.info(`Fetched ${senders.length} verified senders from SendGrid`);
|
|
40
|
+
return senders;
|
|
41
|
+
}
|
|
42
|
+
async validateSender(email) {
|
|
43
|
+
const verifiedSenders = await this.getVerifiedSenders();
|
|
44
|
+
const emailDomain = this.converter.extractDomain(email);
|
|
45
|
+
// Check for exact match
|
|
46
|
+
const exactMatch = verifiedSenders.find(sender => sender.email.toLowerCase() === email.toLowerCase() && sender.isVerified);
|
|
47
|
+
if (exactMatch) {
|
|
48
|
+
return {
|
|
49
|
+
isValid: true,
|
|
50
|
+
verifiedSender: exactMatch,
|
|
51
|
+
domain: emailDomain,
|
|
52
|
+
domainMatches: [exactMatch],
|
|
53
|
+
errors: [],
|
|
54
|
+
suggestions: []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Check for domain matches
|
|
58
|
+
const domainMatches = verifiedSenders.filter(sender => {
|
|
59
|
+
const senderDomain = this.converter.extractDomain(sender.email);
|
|
60
|
+
return this.converter.isDomainMatch(emailDomain, senderDomain) && sender.isVerified;
|
|
61
|
+
});
|
|
62
|
+
if (domainMatches.length > 0) {
|
|
63
|
+
return {
|
|
64
|
+
isValid: true,
|
|
65
|
+
domain: emailDomain,
|
|
66
|
+
domainMatches,
|
|
67
|
+
errors: [],
|
|
68
|
+
suggestions: domainMatches.map(sender => sender.email)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// No matches found
|
|
72
|
+
return {
|
|
73
|
+
isValid: false,
|
|
74
|
+
domain: emailDomain,
|
|
75
|
+
domainMatches: [],
|
|
76
|
+
errors: [`No verified sender found for email ${email} or domain ${emailDomain}`],
|
|
77
|
+
suggestions: this.getSenderSuggestions(email, verifiedSenders)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async selectBestSender(fromEmail) {
|
|
81
|
+
const validation = await this.validateSender(fromEmail);
|
|
82
|
+
if (!validation.isValid || validation.domainMatches.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Prefer exact match
|
|
86
|
+
if (validation.verifiedSender) {
|
|
87
|
+
return validation.verifiedSender;
|
|
88
|
+
}
|
|
89
|
+
// Fall back to best domain match
|
|
90
|
+
// Priority: most recently verified, then alphabetical
|
|
91
|
+
return validation.domainMatches.sort((a, b) => {
|
|
92
|
+
if (a.lastVerifiedAt && b.lastVerifiedAt) {
|
|
93
|
+
return b.lastVerifiedAt.getTime() - a.lastVerifiedAt.getTime();
|
|
94
|
+
}
|
|
95
|
+
if (a.lastVerifiedAt && !b.lastVerifiedAt)
|
|
96
|
+
return -1;
|
|
97
|
+
if (!a.lastVerifiedAt && b.lastVerifiedAt)
|
|
98
|
+
return 1;
|
|
99
|
+
return a.email.localeCompare(b.email);
|
|
100
|
+
})[0];
|
|
101
|
+
}
|
|
102
|
+
clearCache() {
|
|
103
|
+
this.sendersCache = undefined;
|
|
104
|
+
this.cacheTimestamp = undefined;
|
|
105
|
+
this.logger?.debug('Cleared verified senders cache');
|
|
106
|
+
}
|
|
107
|
+
getSenderSuggestions(email, senders) {
|
|
108
|
+
const emailDomain = this.converter.extractDomain(email);
|
|
109
|
+
// Find senders with similar domains
|
|
110
|
+
const suggestions = [];
|
|
111
|
+
for (const sender of senders) {
|
|
112
|
+
if (!sender.isVerified)
|
|
113
|
+
continue;
|
|
114
|
+
const senderDomain = this.converter.extractDomain(sender.email);
|
|
115
|
+
// Check for partial domain matches
|
|
116
|
+
if (emailDomain.includes(senderDomain) ||
|
|
117
|
+
senderDomain.includes(emailDomain)) {
|
|
118
|
+
suggestions.push(sender.email);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return suggestions.slice(0, 5); // Limit to 5 suggestions
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.SenderManager = SenderManager;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { SendGridPluginConfig } from '../types/config-types';
|
|
2
|
+
import { PluginConfig, ProviderCapability, Template, ProviderTemplate, TemplateVersion, SyncResult, ValidationResult, DateRange, TemplateAnalytics, FeatureConfig } from '../types/plugin-types';
|
|
3
|
+
import { VerifiedSender, SenderValidationResult } from '../types/sendgrid-types';
|
|
4
|
+
export interface TemplateProviderPlugin {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly version: string;
|
|
7
|
+
readonly providerId: string;
|
|
8
|
+
initialize(config: PluginConfig): Promise<void>;
|
|
9
|
+
destroy(): Promise<void>;
|
|
10
|
+
createTemplate(template: Template): Promise<string>;
|
|
11
|
+
updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
|
|
12
|
+
deleteTemplate(providerTemplateId: string): Promise<void>;
|
|
13
|
+
getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
|
|
14
|
+
listTemplates(): Promise<ProviderTemplate[]>;
|
|
15
|
+
syncToProvider(template: Template): Promise<SyncResult>;
|
|
16
|
+
syncFromProvider(providerTemplateId: string): Promise<Template>;
|
|
17
|
+
getCapabilities(): ProviderCapability[];
|
|
18
|
+
supportsFeature(feature: string): boolean;
|
|
19
|
+
getFeatureConfig(feature: string): FeatureConfig | null;
|
|
20
|
+
}
|
|
21
|
+
export declare class SendGridTemplatePlugin implements TemplateProviderPlugin {
|
|
22
|
+
readonly name = "sendgrid-templates";
|
|
23
|
+
readonly version = "1.0.0";
|
|
24
|
+
readonly providerId = "sendgrid";
|
|
25
|
+
private client;
|
|
26
|
+
private rateLimiter?;
|
|
27
|
+
private ownershipValidator;
|
|
28
|
+
private prefixEnforcer;
|
|
29
|
+
private templateConverter;
|
|
30
|
+
private senderConverter;
|
|
31
|
+
private templateManager;
|
|
32
|
+
private senderManager;
|
|
33
|
+
private syncManager;
|
|
34
|
+
private readonly config;
|
|
35
|
+
private readonly logger?;
|
|
36
|
+
private initialized;
|
|
37
|
+
constructor(config: SendGridPluginConfig);
|
|
38
|
+
initialize(__pluginConfig: PluginConfig): Promise<void>;
|
|
39
|
+
destroy(): Promise<void>;
|
|
40
|
+
createTemplate(template: Template): Promise<string>;
|
|
41
|
+
updateTemplate(providerTemplateId: string, template: Template): Promise<void>;
|
|
42
|
+
deleteTemplate(providerTemplateId: string): Promise<void>;
|
|
43
|
+
getTemplate(providerTemplateId: string): Promise<ProviderTemplate>;
|
|
44
|
+
listTemplates(): Promise<ProviderTemplate[]>;
|
|
45
|
+
syncToProvider(template: Template): Promise<SyncResult>;
|
|
46
|
+
syncFromProvider(providerTemplateId: string): Promise<Template>;
|
|
47
|
+
getVerifiedSenders(): Promise<VerifiedSender[]>;
|
|
48
|
+
validateSender(email: string): Promise<SenderValidationResult>;
|
|
49
|
+
selectBestSender(email: string): Promise<VerifiedSender | null>;
|
|
50
|
+
createTemplateVersion(templateId: string, version: TemplateVersion): Promise<string>;
|
|
51
|
+
activateTemplateVersion(templateId: string, versionId: string): Promise<void>;
|
|
52
|
+
getTemplateAnalytics(__templateId: string, __dateRange: DateRange): Promise<TemplateAnalytics>;
|
|
53
|
+
batchSyncTemplates(templates: Template[]): Promise<SyncResult[]>;
|
|
54
|
+
getCapabilities(): ProviderCapability[];
|
|
55
|
+
supportsFeature(feature: string): boolean;
|
|
56
|
+
getFeatureConfig(feature: string): FeatureConfig | null;
|
|
57
|
+
validateRoute(__route: unknown): ValidationResult;
|
|
58
|
+
private loadExistingTemplates;
|
|
59
|
+
private ensureInitialized;
|
|
60
|
+
}
|