@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,79 @@
|
|
|
1
|
+
export interface PluginConfig {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
export interface ProviderCapability {
|
|
5
|
+
feature: string;
|
|
6
|
+
description: string;
|
|
7
|
+
methods: string[];
|
|
8
|
+
config: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface Template {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
htmlContent: string;
|
|
16
|
+
textContent?: string;
|
|
17
|
+
category: string;
|
|
18
|
+
tags: string[];
|
|
19
|
+
version: string;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
context?: string;
|
|
22
|
+
variables?: Record<string, unknown>;
|
|
23
|
+
requiredVariables: string[];
|
|
24
|
+
providerTemplateIds?: Record<string, string>;
|
|
25
|
+
lastSyncedAt?: Record<string, Date>;
|
|
26
|
+
syncStatus?: Record<string, string>;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
updatedAt: Date;
|
|
29
|
+
createdBy: string;
|
|
30
|
+
lastModifiedBy: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ProviderTemplate {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
subject: string;
|
|
37
|
+
htmlContent: string;
|
|
38
|
+
textContent?: string;
|
|
39
|
+
version: string;
|
|
40
|
+
active: boolean;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
updatedAt: Date;
|
|
43
|
+
}
|
|
44
|
+
export interface TemplateVersion {
|
|
45
|
+
name: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
htmlContent: string;
|
|
48
|
+
textContent?: string;
|
|
49
|
+
active: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface SyncResult {
|
|
52
|
+
success: boolean;
|
|
53
|
+
action: 'created' | 'updated' | 'deleted' | 'failed';
|
|
54
|
+
providerTemplateId?: string;
|
|
55
|
+
localTemplateId?: string;
|
|
56
|
+
error?: string;
|
|
57
|
+
metadata: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface ValidationResult {
|
|
60
|
+
isValid: boolean;
|
|
61
|
+
errors: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface RouteConflict {
|
|
64
|
+
templateId: string;
|
|
65
|
+
conflicts: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface DateRange {
|
|
68
|
+
start: Date;
|
|
69
|
+
end: Date;
|
|
70
|
+
}
|
|
71
|
+
export interface TemplateAnalytics {
|
|
72
|
+
templateId: string;
|
|
73
|
+
opens: number;
|
|
74
|
+
clicks: number;
|
|
75
|
+
bounces: number;
|
|
76
|
+
deliveries: number;
|
|
77
|
+
dateRange: DateRange;
|
|
78
|
+
}
|
|
79
|
+
export type FeatureConfig = Record<string, unknown>;
|
|
@@ -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 });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface VerifiedSender {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
isVerified: boolean;
|
|
6
|
+
domain: string;
|
|
7
|
+
verificationStatus: 'pending' | 'verified' | 'failed';
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
lastVerifiedAt?: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface SenderValidationResult {
|
|
12
|
+
isValid: boolean;
|
|
13
|
+
verifiedSender?: VerifiedSender;
|
|
14
|
+
domain: string;
|
|
15
|
+
domainMatches: VerifiedSender[];
|
|
16
|
+
errors: string[];
|
|
17
|
+
suggestions: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface SendGridTemplateData {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
generation: 'legacy' | 'dynamic';
|
|
23
|
+
updated_at: string;
|
|
24
|
+
versions?: SendGridTemplateVersion[];
|
|
25
|
+
}
|
|
26
|
+
export interface SendGridTemplateVersion {
|
|
27
|
+
id: string;
|
|
28
|
+
template_id: string;
|
|
29
|
+
active: number;
|
|
30
|
+
name: string;
|
|
31
|
+
html_content: string;
|
|
32
|
+
plain_content?: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
updated_at: string;
|
|
35
|
+
editor: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SendGridVerifiedSenderData {
|
|
38
|
+
id: number;
|
|
39
|
+
nickname: string;
|
|
40
|
+
from_email: string;
|
|
41
|
+
from_name: string;
|
|
42
|
+
reply_to: string;
|
|
43
|
+
reply_to_name: string;
|
|
44
|
+
address: string;
|
|
45
|
+
address2: string;
|
|
46
|
+
city: string;
|
|
47
|
+
state: string;
|
|
48
|
+
zip: string;
|
|
49
|
+
country: string;
|
|
50
|
+
verified: boolean;
|
|
51
|
+
locked: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface SendGridApiError extends Error {
|
|
54
|
+
statusCode?: number;
|
|
55
|
+
response?: unknown;
|
|
56
|
+
}
|
|
@@ -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 });
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
preset: 'ts-jest',
|
|
11
|
+
testEnvironment: 'node',
|
|
12
|
+
collectCoverageFrom: [
|
|
13
|
+
'src/**/*.{ts,tsx}',
|
|
14
|
+
'!src/**/*.d.ts',
|
|
15
|
+
'!src/index.ts'
|
|
16
|
+
],
|
|
17
|
+
coverageThreshold: {
|
|
18
|
+
global: {
|
|
19
|
+
branches: 60,
|
|
20
|
+
functions: 75,
|
|
21
|
+
lines: 65,
|
|
22
|
+
statements: 65
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bernierllc/email-sendgrid-plugin",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "SendGrid-specific template management, sender verification, and advanced features plugin for email template service",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"clean": "rm -rf dist",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"test:coverage": "jest --coverage",
|
|
13
|
+
"lint": "eslint src --ext .ts",
|
|
14
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sendgrid",
|
|
18
|
+
"email",
|
|
19
|
+
"template",
|
|
20
|
+
"plugin",
|
|
21
|
+
"sender-verification",
|
|
22
|
+
"template-management"
|
|
23
|
+
],
|
|
24
|
+
"author": "Bernier LLC",
|
|
25
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@sendgrid/mail": "^8.1.3",
|
|
28
|
+
"@bernierllc/logger": "^1.0.1",
|
|
29
|
+
"@bernierllc/retry-policy": "^0.1.3",
|
|
30
|
+
"@bernierllc/rate-limiter": "^0.1.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/jest": "^29.5.12",
|
|
34
|
+
"@types/node": "^20.11.30",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
|
36
|
+
"@typescript-eslint/parser": "^7.3.1",
|
|
37
|
+
"eslint": "^8.57.0",
|
|
38
|
+
"jest": "^29.7.0",
|
|
39
|
+
"ts-jest": "^29.1.2",
|
|
40
|
+
"typescript": "^5.4.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 SendGridApiError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
public statusCode?: number,
|
|
13
|
+
public response?: unknown
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'SendGridApiError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TemplateNotFoundError extends Error {
|
|
21
|
+
constructor(public templateId: string) {
|
|
22
|
+
super(`Template not found: ${templateId}`);
|
|
23
|
+
this.name = 'TemplateNotFoundError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class UnauthorizedTemplateAccessError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'UnauthorizedTemplateAccessError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class TemplateConflictError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
public templateId: string,
|
|
37
|
+
public conflicts: string[]
|
|
38
|
+
) {
|
|
39
|
+
super(`Template conflicts detected: ${conflicts.join(', ')}`);
|
|
40
|
+
this.name = 'TemplateConflictError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class RateLimitError extends Error {
|
|
45
|
+
constructor(message: string) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'RateLimitError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { RateLimitError } from './errors';
|
|
10
|
+
|
|
11
|
+
export interface RateLimiterConfig {
|
|
12
|
+
maxRequestsPerSecond: number;
|
|
13
|
+
maxRequestsPerMinute: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SimpleRateLimiter {
|
|
17
|
+
private requestTimestamps: number[] = [];
|
|
18
|
+
private readonly maxRequestsPerSecond: number;
|
|
19
|
+
private readonly maxRequestsPerMinute: number;
|
|
20
|
+
|
|
21
|
+
constructor(config: RateLimiterConfig) {
|
|
22
|
+
this.maxRequestsPerSecond = config.maxRequestsPerSecond;
|
|
23
|
+
this.maxRequestsPerMinute = config.maxRequestsPerMinute;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async acquire(): Promise<void> {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
|
|
29
|
+
// Clean up old timestamps
|
|
30
|
+
this.requestTimestamps = this.requestTimestamps.filter(
|
|
31
|
+
timestamp => now - timestamp < 60000 // Keep last minute
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Check per-second limit
|
|
35
|
+
const lastSecond = this.requestTimestamps.filter(
|
|
36
|
+
timestamp => now - timestamp < 1000
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (lastSecond.length >= this.maxRequestsPerSecond) {
|
|
40
|
+
const waitTime = 1000 - (now - lastSecond[0]);
|
|
41
|
+
await this.sleep(waitTime);
|
|
42
|
+
return this.acquire();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check per-minute limit
|
|
46
|
+
if (this.requestTimestamps.length >= this.maxRequestsPerMinute) {
|
|
47
|
+
const waitTime = 60000 - (now - this.requestTimestamps[0]);
|
|
48
|
+
if (waitTime > 5000) {
|
|
49
|
+
throw new RateLimitError(`Rate limit exceeded, would need to wait ${Math.ceil(waitTime / 1000)}s`);
|
|
50
|
+
}
|
|
51
|
+
await this.sleep(waitTime);
|
|
52
|
+
return this.acquire();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add timestamp and proceed
|
|
56
|
+
this.requestTimestamps.push(now);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
release(): void {
|
|
60
|
+
// No-op for simple implementation
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private sleep(ms: number): Promise<void> {
|
|
64
|
+
return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getRemainingCapacity(): { perSecond: number; perMinute: number } {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const lastSecond = this.requestTimestamps.filter(
|
|
70
|
+
timestamp => now - timestamp < 1000
|
|
71
|
+
).length;
|
|
72
|
+
const lastMinute = this.requestTimestamps.length;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
perSecond: Math.max(0, this.maxRequestsPerSecond - lastSecond),
|
|
76
|
+
perMinute: Math.max(0, this.maxRequestsPerMinute - lastMinute)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { ApiRequestOptions, ApiResponse } from '../types/config-types';
|
|
10
|
+
import { SendGridApiError } from './errors';
|
|
11
|
+
import { SimpleRateLimiter } from './rate-limiter';
|
|
12
|
+
|
|
13
|
+
export interface SendGridClientConfig {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
rateLimiter?: SimpleRateLimiter;
|
|
16
|
+
logger?: {
|
|
17
|
+
debug: (message: string) => void;
|
|
18
|
+
error: (message: string) => void;
|
|
19
|
+
info: (message: string) => void;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SendGridClient {
|
|
24
|
+
private readonly apiKey: string;
|
|
25
|
+
private readonly rateLimiter?: SimpleRateLimiter;
|
|
26
|
+
private readonly logger?: SendGridClientConfig['logger'];
|
|
27
|
+
private readonly baseUrl = 'https://api.sendgrid.com/v3';
|
|
28
|
+
|
|
29
|
+
constructor(config: SendGridClientConfig) {
|
|
30
|
+
this.apiKey = config.apiKey;
|
|
31
|
+
this.rateLimiter = config.rateLimiter;
|
|
32
|
+
this.logger = config.logger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async request(endpoint: string, options: ApiRequestOptions): Promise<ApiResponse> {
|
|
36
|
+
// Apply rate limiting if configured
|
|
37
|
+
if (this.rateLimiter) {
|
|
38
|
+
await this.rateLimiter.acquire();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
43
|
+
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method: options.method,
|
|
46
|
+
headers: {
|
|
47
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
...options.headers
|
|
50
|
+
},
|
|
51
|
+
body: options.body
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const body = await response.text();
|
|
55
|
+
|
|
56
|
+
// Log all API calls for audit trail
|
|
57
|
+
this.logger?.debug(`SendGrid API ${options.method} ${endpoint}: ${response.status}`);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
statusCode: response.status,
|
|
61
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
62
|
+
body
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
66
|
+
this.logger?.error(`SendGrid API error: ${errorMessage}`);
|
|
67
|
+
throw new SendGridApiError(`API request failed: ${errorMessage}`, undefined, error);
|
|
68
|
+
} finally {
|
|
69
|
+
if (this.rateLimiter) {
|
|
70
|
+
this.rateLimiter.release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isSuccessResponse(response: ApiResponse): boolean {
|
|
76
|
+
return response.statusCode >= 200 && response.statusCode < 300;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parseResponseBody<T>(response: ApiResponse): T {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(response.body) as T;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new SendGridApiError('Failed to parse response body', response.statusCode, response.body);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { VerifiedSender, SendGridVerifiedSenderData } from '../types/sendgrid-types';
|
|
10
|
+
|
|
11
|
+
export class SenderConverter {
|
|
12
|
+
convertToVerifiedSender(sendgridData: SendGridVerifiedSenderData): VerifiedSender {
|
|
13
|
+
const domain = this.extractDomain(sendgridData.from_email);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
id: sendgridData.id.toString(),
|
|
17
|
+
email: sendgridData.from_email,
|
|
18
|
+
name: sendgridData.from_name || sendgridData.nickname,
|
|
19
|
+
isVerified: sendgridData.verified,
|
|
20
|
+
domain,
|
|
21
|
+
verificationStatus: sendgridData.verified ? 'verified' : 'pending',
|
|
22
|
+
createdAt: new Date(), // SendGrid doesn't provide creation date
|
|
23
|
+
lastVerifiedAt: sendgridData.verified ? new Date() : undefined
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extractDomain(email: string): string {
|
|
28
|
+
return email.split('@')[1]?.toLowerCase() || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isDomainMatch(domain1: string, domain2: string): boolean {
|
|
32
|
+
// Exact match
|
|
33
|
+
if (domain1 === domain2) return true;
|
|
34
|
+
|
|
35
|
+
// Subdomain match (e.g., app.example.com matches example.com)
|
|
36
|
+
if (domain1.endsWith(`.${domain2}`)) return true;
|
|
37
|
+
if (domain2.endsWith(`.${domain1}`)) return true;
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -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
|
+
import {
|
|
10
|
+
SendGridTemplateData,
|
|
11
|
+
SendGridTemplateVersion
|
|
12
|
+
} from '../types/sendgrid-types';
|
|
13
|
+
import { ProviderTemplate, Template } from '../types/plugin-types';
|
|
14
|
+
import { PrefixEnforcer } from '../safety/prefix-enforcer';
|
|
15
|
+
|
|
16
|
+
export class TemplateConverter {
|
|
17
|
+
private readonly prefixEnforcer: PrefixEnforcer;
|
|
18
|
+
|
|
19
|
+
constructor(prefixEnforcer: PrefixEnforcer) {
|
|
20
|
+
this.prefixEnforcer = prefixEnforcer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
convertToProviderTemplate(
|
|
24
|
+
sendgridData: SendGridTemplateData,
|
|
25
|
+
activeVersion?: SendGridTemplateVersion
|
|
26
|
+
): ProviderTemplate {
|
|
27
|
+
// Find active version if not provided
|
|
28
|
+
const version = activeVersion || this.findActiveVersion(sendgridData.versions || []);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
id: sendgridData.id,
|
|
32
|
+
name: this.prefixEnforcer.stripPrefix(sendgridData.name),
|
|
33
|
+
subject: version?.subject || '',
|
|
34
|
+
htmlContent: version?.html_content || '',
|
|
35
|
+
textContent: version?.plain_content,
|
|
36
|
+
version: version?.name || '1.0.0',
|
|
37
|
+
active: version?.active === 1,
|
|
38
|
+
createdAt: new Date(sendgridData.updated_at),
|
|
39
|
+
updatedAt: new Date(sendgridData.updated_at)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
convertToLocalTemplate(
|
|
44
|
+
sendgridData: SendGridTemplateData,
|
|
45
|
+
localId: string,
|
|
46
|
+
activeVersion?: SendGridTemplateVersion
|
|
47
|
+
): Template {
|
|
48
|
+
const providerTemplate = this.convertToProviderTemplate(sendgridData, activeVersion);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: localId,
|
|
52
|
+
name: providerTemplate.name,
|
|
53
|
+
subject: providerTemplate.subject,
|
|
54
|
+
htmlContent: providerTemplate.htmlContent,
|
|
55
|
+
textContent: providerTemplate.textContent,
|
|
56
|
+
category: 'synced',
|
|
57
|
+
tags: ['sendgrid', 'synced'],
|
|
58
|
+
version: providerTemplate.version,
|
|
59
|
+
isActive: providerTemplate.active,
|
|
60
|
+
requiredVariables: [],
|
|
61
|
+
providerTemplateIds: {
|
|
62
|
+
sendgrid: sendgridData.id
|
|
63
|
+
},
|
|
64
|
+
lastSyncedAt: {
|
|
65
|
+
sendgrid: new Date()
|
|
66
|
+
},
|
|
67
|
+
syncStatus: {
|
|
68
|
+
sendgrid: 'success'
|
|
69
|
+
},
|
|
70
|
+
createdAt: providerTemplate.createdAt,
|
|
71
|
+
updatedAt: providerTemplate.updatedAt,
|
|
72
|
+
createdBy: 'sendgrid-sync',
|
|
73
|
+
lastModifiedBy: 'sendgrid-sync'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
extractVariables(content: string): string[] {
|
|
78
|
+
// Extract Handlebars-style variables: {{variableName}}
|
|
79
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
80
|
+
const matches = content.matchAll(regex);
|
|
81
|
+
const variables = new Set<string>();
|
|
82
|
+
|
|
83
|
+
for (const match of matches) {
|
|
84
|
+
const variable = match[1].trim();
|
|
85
|
+
// Filter out Handlebars helpers
|
|
86
|
+
if (!variable.startsWith('#') && !variable.startsWith('/')) {
|
|
87
|
+
variables.add(variable);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Array.from(variables);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private findActiveVersion(versions: SendGridTemplateVersion[]): SendGridTemplateVersion | undefined {
|
|
95
|
+
return versions.find(v => v.active === 1) || versions[0];
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
// Main plugin class
|
|
10
|
+
export { SendGridTemplatePlugin, TemplateProviderPlugin } from './plugin/sendgrid-plugin';
|
|
11
|
+
|
|
12
|
+
// Plugin components
|
|
13
|
+
export { TemplateManager } from './plugin/template-manager';
|
|
14
|
+
export { SenderManager } from './plugin/sender-manager';
|
|
15
|
+
export { SyncManager } from './plugin/sync-manager';
|
|
16
|
+
|
|
17
|
+
// API components
|
|
18
|
+
export { SendGridClient } from './api/sendgrid-client';
|
|
19
|
+
export { SimpleRateLimiter } from './api/rate-limiter';
|
|
20
|
+
|
|
21
|
+
// Safety components
|
|
22
|
+
export { OwnershipValidator, SafetyMode } from './safety/ownership-validator';
|
|
23
|
+
export { PrefixEnforcer } from './safety/prefix-enforcer';
|
|
24
|
+
|
|
25
|
+
// Converters
|
|
26
|
+
export { TemplateConverter } from './converters/template-converter';
|
|
27
|
+
export { SenderConverter } from './converters/sender-converter';
|
|
28
|
+
|
|
29
|
+
// Errors
|
|
30
|
+
export {
|
|
31
|
+
SendGridApiError,
|
|
32
|
+
TemplateNotFoundError,
|
|
33
|
+
UnauthorizedTemplateAccessError,
|
|
34
|
+
TemplateConflictError,
|
|
35
|
+
RateLimitError
|
|
36
|
+
} from './api/errors';
|
|
37
|
+
|
|
38
|
+
// Types
|
|
39
|
+
export type { SendGridPluginConfig, ApiRequestOptions, ApiResponse } from './types/config-types';
|
|
40
|
+
export type {
|
|
41
|
+
PluginConfig,
|
|
42
|
+
ProviderCapability,
|
|
43
|
+
Template,
|
|
44
|
+
ProviderTemplate,
|
|
45
|
+
TemplateVersion,
|
|
46
|
+
SyncResult,
|
|
47
|
+
ValidationResult,
|
|
48
|
+
DateRange,
|
|
49
|
+
TemplateAnalytics,
|
|
50
|
+
FeatureConfig
|
|
51
|
+
} from './types/plugin-types';
|
|
52
|
+
export type {
|
|
53
|
+
VerifiedSender,
|
|
54
|
+
SenderValidationResult,
|
|
55
|
+
SendGridTemplateData,
|
|
56
|
+
SendGridTemplateVersion,
|
|
57
|
+
SendGridVerifiedSenderData
|
|
58
|
+
} from './types/sendgrid-types';
|