glasswork 0.10.0 → 0.11.0
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/dist/email/compiler/code-generator.d.ts +11 -0
- package/dist/email/compiler/code-generator.d.ts.map +1 -0
- package/dist/email/compiler/code-generator.js +319 -0
- package/dist/email/compiler/code-generator.js.map +1 -0
- package/dist/email/compiler/compile-templates.d.ts +68 -0
- package/dist/email/compiler/compile-templates.d.ts.map +1 -0
- package/dist/email/compiler/compile-templates.js +156 -0
- package/dist/email/compiler/compile-templates.js.map +1 -0
- package/dist/email/compiler/compiler.d.ts +30 -0
- package/dist/email/compiler/compiler.d.ts.map +1 -0
- package/dist/email/compiler/compiler.js +510 -0
- package/dist/email/compiler/compiler.js.map +1 -0
- package/dist/email/compiler/index.d.ts +4 -0
- package/dist/email/compiler/index.d.ts.map +1 -0
- package/dist/email/compiler/index.js +3 -0
- package/dist/email/compiler/index.js.map +1 -0
- package/dist/email/compiler/tokenizer.d.ts +7 -0
- package/dist/email/compiler/tokenizer.d.ts.map +1 -0
- package/dist/email/compiler/tokenizer.js +154 -0
- package/dist/email/compiler/tokenizer.js.map +1 -0
- package/dist/email/compiler/type-extractor.d.ts +11 -0
- package/dist/email/compiler/type-extractor.d.ts.map +1 -0
- package/dist/email/compiler/type-extractor.js +296 -0
- package/dist/email/compiler/type-extractor.js.map +1 -0
- package/dist/email/compiler/types.d.ts +108 -0
- package/dist/email/compiler/types.d.ts.map +1 -0
- package/dist/email/compiler/types.js +2 -0
- package/dist/email/compiler/types.js.map +1 -0
- package/dist/email/config.d.ts +89 -0
- package/dist/email/config.d.ts.map +1 -0
- package/dist/email/config.js +55 -0
- package/dist/email/config.js.map +1 -0
- package/dist/email/email-service.d.ts +54 -0
- package/dist/email/email-service.d.ts.map +1 -0
- package/dist/email/email-service.js +94 -0
- package/dist/email/email-service.js.map +1 -0
- package/dist/email/index.d.ts +12 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +23 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/template-registry.d.ts +83 -0
- package/dist/email/template-registry.d.ts.map +1 -0
- package/dist/email/template-registry.js +85 -0
- package/dist/email/template-registry.js.map +1 -0
- package/dist/email/templated-email-service.d.ts +59 -0
- package/dist/email/templated-email-service.d.ts.map +1 -0
- package/dist/email/templated-email-service.js +63 -0
- package/dist/email/templated-email-service.js.map +1 -0
- package/dist/email/transports/index.d.ts +3 -0
- package/dist/email/transports/index.d.ts.map +1 -0
- package/dist/email/transports/index.js +3 -0
- package/dist/email/transports/index.js.map +1 -0
- package/dist/email/transports/mock-transport.d.ts +74 -0
- package/dist/email/transports/mock-transport.d.ts.map +1 -0
- package/dist/email/transports/mock-transport.js +102 -0
- package/dist/email/transports/mock-transport.js.map +1 -0
- package/dist/email/transports/ses-transport.d.ts +65 -0
- package/dist/email/transports/ses-transport.d.ts.map +1 -0
- package/dist/email/transports/ses-transport.js +290 -0
- package/dist/email/transports/ses-transport.js.map +1 -0
- package/dist/email/types.d.ts +128 -0
- package/dist/email/types.d.ts.map +1 -0
- package/dist/email/types.js +2 -0
- package/dist/email/types.js.map +1 -0
- package/dist/email/webhooks/handler-factory.d.ts +41 -0
- package/dist/email/webhooks/handler-factory.d.ts.map +1 -0
- package/dist/email/webhooks/handler-factory.js +136 -0
- package/dist/email/webhooks/handler-factory.js.map +1 -0
- package/dist/email/webhooks/index.d.ts +6 -0
- package/dist/email/webhooks/index.d.ts.map +1 -0
- package/dist/email/webhooks/index.js +9 -0
- package/dist/email/webhooks/index.js.map +1 -0
- package/dist/email/webhooks/notification-parser.d.ts +45 -0
- package/dist/email/webhooks/notification-parser.d.ts.map +1 -0
- package/dist/email/webhooks/notification-parser.js +150 -0
- package/dist/email/webhooks/notification-parser.js.map +1 -0
- package/dist/email/webhooks/signature-verification.d.ts +38 -0
- package/dist/email/webhooks/signature-verification.d.ts.map +1 -0
- package/dist/email/webhooks/signature-verification.js +179 -0
- package/dist/email/webhooks/signature-verification.js.map +1 -0
- package/dist/email/webhooks/subscription-handler.d.ts +37 -0
- package/dist/email/webhooks/subscription-handler.d.ts.map +1 -0
- package/dist/email/webhooks/subscription-handler.js +99 -0
- package/dist/email/webhooks/subscription-handler.js.map +1 -0
- package/dist/email/webhooks/types.d.ts +187 -0
- package/dist/email/webhooks/types.d.ts.map +1 -0
- package/dist/email/webhooks/types.js +5 -0
- package/dist/email/webhooks/types.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email module types and interfaces
|
|
3
|
+
*/
|
|
4
|
+
import type { SendEmailCommandInput } from '@aws-sdk/client-sesv2';
|
|
5
|
+
/**
|
|
6
|
+
* Attachment for an email message
|
|
7
|
+
*/
|
|
8
|
+
export interface EmailAttachment {
|
|
9
|
+
/** Filename for the attachment */
|
|
10
|
+
filename: string;
|
|
11
|
+
/** Content of the attachment (base64 encoded or Buffer) */
|
|
12
|
+
content: string | Buffer;
|
|
13
|
+
/** MIME type of the attachment */
|
|
14
|
+
contentType?: string;
|
|
15
|
+
/** Content disposition (attachment or inline) */
|
|
16
|
+
disposition?: 'attachment' | 'inline';
|
|
17
|
+
/** Content ID for inline attachments */
|
|
18
|
+
contentId?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Email message to be sent
|
|
22
|
+
*/
|
|
23
|
+
export interface EmailMessage {
|
|
24
|
+
/** Recipient email address(es) */
|
|
25
|
+
to: string | string[];
|
|
26
|
+
/** Email subject */
|
|
27
|
+
subject: string;
|
|
28
|
+
/** HTML body content */
|
|
29
|
+
html: string;
|
|
30
|
+
/** Plain text body content (auto-generated if not provided) */
|
|
31
|
+
text?: string;
|
|
32
|
+
/** Sender email address (overrides default) */
|
|
33
|
+
from?: string;
|
|
34
|
+
/** Reply-to email address */
|
|
35
|
+
replyTo?: string;
|
|
36
|
+
/** CC recipients */
|
|
37
|
+
cc?: string | string[];
|
|
38
|
+
/** BCC recipients */
|
|
39
|
+
bcc?: string | string[];
|
|
40
|
+
/** Email attachments */
|
|
41
|
+
attachments?: EmailAttachment[];
|
|
42
|
+
/** Custom headers */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result of sending an email
|
|
47
|
+
*/
|
|
48
|
+
export interface EmailResult {
|
|
49
|
+
/** Message ID returned by the email provider */
|
|
50
|
+
messageId: string;
|
|
51
|
+
/** Whether the send was successful */
|
|
52
|
+
success: boolean;
|
|
53
|
+
/** Additional metadata from the provider */
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Email transport interface for sending emails
|
|
58
|
+
*/
|
|
59
|
+
export interface EmailTransport {
|
|
60
|
+
/** Transport name for logging/debugging */
|
|
61
|
+
readonly name: string;
|
|
62
|
+
/**
|
|
63
|
+
* Sends an email message
|
|
64
|
+
* @param message - The email message to send
|
|
65
|
+
* @returns Promise resolving to the send result
|
|
66
|
+
*/
|
|
67
|
+
send(message: EmailMessage): Promise<EmailResult>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* SES transport configuration
|
|
71
|
+
*/
|
|
72
|
+
export interface SESTransportConfig {
|
|
73
|
+
/** AWS region for SES */
|
|
74
|
+
region: string;
|
|
75
|
+
/** SES configuration set name (enables delivery tracking) */
|
|
76
|
+
configurationSet?: string;
|
|
77
|
+
/** Custom endpoint (for local testing with LocalStack) */
|
|
78
|
+
endpoint?: string;
|
|
79
|
+
/** Optional pre-configured SES client (useful for testing/mocking) */
|
|
80
|
+
client?: {
|
|
81
|
+
send(command: unknown): Promise<{
|
|
82
|
+
MessageId?: string;
|
|
83
|
+
}>;
|
|
84
|
+
};
|
|
85
|
+
/** Optional SendEmailCommand constructor (useful for testing/mocking) */
|
|
86
|
+
sendEmailCommand?: new (input: SendEmailCommandInput) => unknown;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* SMTP transport configuration
|
|
90
|
+
*/
|
|
91
|
+
export interface SMTPTransportConfig {
|
|
92
|
+
/** SMTP host */
|
|
93
|
+
host: string;
|
|
94
|
+
/** SMTP port */
|
|
95
|
+
port: number;
|
|
96
|
+
/** Use TLS */
|
|
97
|
+
secure?: boolean;
|
|
98
|
+
/** Authentication credentials */
|
|
99
|
+
auth?: {
|
|
100
|
+
user: string;
|
|
101
|
+
pass: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Email module configuration
|
|
106
|
+
*/
|
|
107
|
+
export interface EmailConfig {
|
|
108
|
+
/** Default sender email address */
|
|
109
|
+
from: string;
|
|
110
|
+
/** Default reply-to address */
|
|
111
|
+
replyTo?: string;
|
|
112
|
+
/** Transport configuration */
|
|
113
|
+
transport: EmailTransport;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hook called after an email is successfully sent
|
|
117
|
+
*/
|
|
118
|
+
export type OnSentHook = (result: EmailResult, message: EmailMessage) => Promise<void> | void;
|
|
119
|
+
/**
|
|
120
|
+
* Full email module options
|
|
121
|
+
*/
|
|
122
|
+
export interface EmailModuleOptions {
|
|
123
|
+
/** Email configuration */
|
|
124
|
+
config: EmailConfig;
|
|
125
|
+
/** Hook called after successful send (for tracking) */
|
|
126
|
+
onSent?: OnSentHook;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/email/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/email/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { CreateWebhookHandlerOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a complete SES webhook handler with signature verification,
|
|
5
|
+
* subscription handling, and event parsing.
|
|
6
|
+
*
|
|
7
|
+
* This is a convenience factory that combines the individual middleware
|
|
8
|
+
* components. For more control, use the individual utilities directly.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createSESWebhookHandler } from 'glasswork';
|
|
13
|
+
*
|
|
14
|
+
* router.post('/webhooks/ses',
|
|
15
|
+
* createSESWebhookHandler({
|
|
16
|
+
* onDelivered: async (event, c) => {
|
|
17
|
+
* const logger = c.get('logger');
|
|
18
|
+
* logger.info({ messageId: event.messageId }, 'Email delivered');
|
|
19
|
+
* await db.email.update({
|
|
20
|
+
* where: { messageId: event.messageId },
|
|
21
|
+
* data: { status: 'DELIVERED', deliveredAt: event.timestamp },
|
|
22
|
+
* });
|
|
23
|
+
* },
|
|
24
|
+
* onBounced: async (event, c) => {
|
|
25
|
+
* await db.email.update({
|
|
26
|
+
* where: { messageId: event.messageId },
|
|
27
|
+
* data: { status: 'BOUNCED', bounceType: event.bounceType },
|
|
28
|
+
* });
|
|
29
|
+
* },
|
|
30
|
+
* onComplaint: async (event, c) => {
|
|
31
|
+
* await db.email.update({
|
|
32
|
+
* where: { messageId: event.messageId },
|
|
33
|
+
* data: { status: 'COMPLAINT' },
|
|
34
|
+
* });
|
|
35
|
+
* },
|
|
36
|
+
* })
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function createSESWebhookHandler(options?: CreateWebhookHandlerOptions): MiddlewareHandler;
|
|
41
|
+
//# sourceMappingURL=handler-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-factory.d.ts","sourceRoot":"","sources":["../../../src/email/webhooks/handler-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAKvD,OAAO,KAAK,EAGV,2BAA2B,EAG5B,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CA8BnB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createLogger } from '../../utils/logger.js';
|
|
2
|
+
import { parseSESNotification } from './notification-parser.js';
|
|
3
|
+
import { verifySNSSignature } from './signature-verification.js';
|
|
4
|
+
import { handleSNSSubscription } from './subscription-handler.js';
|
|
5
|
+
const logger = createLogger('SES Webhook');
|
|
6
|
+
/**
|
|
7
|
+
* Creates a complete SES webhook handler with signature verification,
|
|
8
|
+
* subscription handling, and event parsing.
|
|
9
|
+
*
|
|
10
|
+
* This is a convenience factory that combines the individual middleware
|
|
11
|
+
* components. For more control, use the individual utilities directly.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createSESWebhookHandler } from 'glasswork';
|
|
16
|
+
*
|
|
17
|
+
* router.post('/webhooks/ses',
|
|
18
|
+
* createSESWebhookHandler({
|
|
19
|
+
* onDelivered: async (event, c) => {
|
|
20
|
+
* const logger = c.get('logger');
|
|
21
|
+
* logger.info({ messageId: event.messageId }, 'Email delivered');
|
|
22
|
+
* await db.email.update({
|
|
23
|
+
* where: { messageId: event.messageId },
|
|
24
|
+
* data: { status: 'DELIVERED', deliveredAt: event.timestamp },
|
|
25
|
+
* });
|
|
26
|
+
* },
|
|
27
|
+
* onBounced: async (event, c) => {
|
|
28
|
+
* await db.email.update({
|
|
29
|
+
* where: { messageId: event.messageId },
|
|
30
|
+
* data: { status: 'BOUNCED', bounceType: event.bounceType },
|
|
31
|
+
* });
|
|
32
|
+
* },
|
|
33
|
+
* onComplaint: async (event, c) => {
|
|
34
|
+
* await db.email.update({
|
|
35
|
+
* where: { messageId: event.messageId },
|
|
36
|
+
* data: { status: 'COMPLAINT' },
|
|
37
|
+
* });
|
|
38
|
+
* },
|
|
39
|
+
* })
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function createSESWebhookHandler(options = {}) {
|
|
44
|
+
const { verifySignature = process.env.NODE_ENV === 'production', signatureOptions, onDelivered, onBounced, onComplaint, } = options;
|
|
45
|
+
return async (c, _next) => {
|
|
46
|
+
// Step 1: Verify signature (optional, enabled by default in production)
|
|
47
|
+
const verifyResult = await handleSignatureVerification(c, verifySignature, signatureOptions);
|
|
48
|
+
if (verifyResult)
|
|
49
|
+
return verifyResult;
|
|
50
|
+
// Step 2: Handle subscription confirmations
|
|
51
|
+
const subscriptionResult = await handleSubscriptionConfirmation(c);
|
|
52
|
+
if (subscriptionResult)
|
|
53
|
+
return subscriptionResult;
|
|
54
|
+
// Step 3: Parse the SES event
|
|
55
|
+
const event = await parseSESNotification(c);
|
|
56
|
+
if (!event) {
|
|
57
|
+
// Not an SES notification we handle (e.g., Send event)
|
|
58
|
+
return c.json({ received: true }, 200);
|
|
59
|
+
}
|
|
60
|
+
// Step 4: Route to the appropriate handler
|
|
61
|
+
await routeSESEvent(event, c, { onDelivered, onBounced, onComplaint });
|
|
62
|
+
return c.json({ received: true }, 200);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Handles signature verification step
|
|
67
|
+
*/
|
|
68
|
+
async function handleSignatureVerification(c, verifySignature, signatureOptions) {
|
|
69
|
+
if (verifySignature) {
|
|
70
|
+
const verifyMiddleware = verifySNSSignature(signatureOptions);
|
|
71
|
+
return await runMiddleware(verifyMiddleware, c);
|
|
72
|
+
}
|
|
73
|
+
// Still parse the body even without verification
|
|
74
|
+
try {
|
|
75
|
+
const body = await c.req.text();
|
|
76
|
+
c.set('snsMessageRaw', body);
|
|
77
|
+
c.set('snsMessage', JSON.parse(body));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Handles subscription confirmation step
|
|
86
|
+
*/
|
|
87
|
+
async function handleSubscriptionConfirmation(c) {
|
|
88
|
+
const subscriptionMiddleware = handleSNSSubscription();
|
|
89
|
+
return await runMiddleware(subscriptionMiddleware, c);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Routes SES events to appropriate handlers
|
|
93
|
+
*/
|
|
94
|
+
async function routeSESEvent(event, c, handlers) {
|
|
95
|
+
try {
|
|
96
|
+
switch (event.type) {
|
|
97
|
+
case 'delivered':
|
|
98
|
+
if (handlers.onDelivered) {
|
|
99
|
+
await handlers.onDelivered(event, c);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'bounced':
|
|
103
|
+
if (handlers.onBounced) {
|
|
104
|
+
await handlers.onBounced(event, c);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case 'complaint':
|
|
108
|
+
if (handlers.onComplaint) {
|
|
109
|
+
await handlers.onComplaint(event, c);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.error('Handler error:', error);
|
|
116
|
+
// Don't fail the request - we don't want SNS to retry indefinitely
|
|
117
|
+
// The error is logged and can be tracked
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Helper to run middleware and capture early responses
|
|
122
|
+
*/
|
|
123
|
+
async function runMiddleware(middleware, c) {
|
|
124
|
+
let earlyResponse;
|
|
125
|
+
let nextCalled = false;
|
|
126
|
+
await middleware(c, async () => {
|
|
127
|
+
nextCalled = true;
|
|
128
|
+
});
|
|
129
|
+
// Check if middleware returned early (didn't call next)
|
|
130
|
+
if (!nextCalled) {
|
|
131
|
+
// The middleware returned a response
|
|
132
|
+
earlyResponse = c.res;
|
|
133
|
+
}
|
|
134
|
+
return earlyResponse;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=handler-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../../src/email/webhooks/handler-factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AASlE,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,uBAAuB,CACrC,UAAuC,EAAE;IAEzC,MAAM,EACJ,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EACvD,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,WAAW,GACZ,GAAG,OAAO,CAAC;IAEZ,OAAO,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE;QACxB,wEAAwE;QACxE,MAAM,YAAY,GAAG,MAAM,2BAA2B,CAAC,CAAC,EAAE,eAAe,EAAE,gBAAgB,CAAC,CAAC;QAC7F,IAAI,YAAY;YAAE,OAAO,YAAY,CAAC;QAEtC,4CAA4C;QAC5C,MAAM,kBAAkB,GAAG,MAAM,8BAA8B,CAAC,CAAC,CAAC,CAAC;QACnE,IAAI,kBAAkB;YAAE,OAAO,kBAAkB,CAAC;QAElD,8BAA8B;QAC9B,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,CAAC,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,uDAAuD;YACvD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;QACzC,CAAC;QAED,2CAA2C;QAC3C,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAEvE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;IACzC,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,2BAA2B,CACxC,CAAU,EACV,eAAwB,EACxB,gBAA2D;IAE3D,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;QAC9D,OAAO,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,iDAAiD;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAChC,CAAC,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,8BAA8B,CAAC,CAAU;IACtD,MAAM,sBAAsB,GAAG,qBAAqB,EAAE,CAAC;IACvD,OAAO,MAAM,aAAa,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,aAAa,CAC1B,KAAe,EACf,CAAU,EACV,QAIC;IAED,IAAI,CAAC;QACH,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,WAAW;gBACd,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;oBACzB,MAAM,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACvC,CAAC;gBACD,MAAM;YACR,KAAK,SAAS;gBACZ,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACvB,MAAM,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACrC,CAAC;gBACD,MAAM;YACR,KAAK,WAAW;gBACd,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;oBACzB,MAAM,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACvC,CAAC;gBACD,MAAM;QACV,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;QACtC,mEAAmE;QACnE,yCAAyC;IAC3C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,aAAa,CAC1B,UAA6B,EAC7B,CAAU;IAEV,IAAI,aAAmC,CAAC;IACxC,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,MAAM,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE;QAC7B,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,wDAAwD;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,qCAAqC;QACrC,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC;IACxB,CAAC;IAED,OAAO,aAAa,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createSESWebhookHandler } from './handler-factory.js';
|
|
2
|
+
export { parseSESNotification, parseSNSMessage } from './notification-parser.js';
|
|
3
|
+
export { clearCertCache, verifySNSSignature, } from './signature-verification.js';
|
|
4
|
+
export { type HandleSubscriptionOptions, handleSNSSubscription, } from './subscription-handler.js';
|
|
5
|
+
export type { BouncedEvent, ComplaintEvent, CreateWebhookHandlerOptions, DeliveredEvent, SESBounceNotification, SESComplaintNotification, SESDeliveryNotification, SESEvent, SESMailObject, SESNotification, SESWebhookHandlers, SNSMessage, VerifySignatureOptions, } from './types.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/email/webhooks/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEjF,OAAO,EACL,cAAc,EACd,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,yBAAyB,EAC9B,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,YAAY,EACZ,cAAc,EACd,2BAA2B,EAC3B,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,UAAU,EACV,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Webhook types
|
|
2
|
+
// Handler factory
|
|
3
|
+
export { createSESWebhookHandler } from './handler-factory.js';
|
|
4
|
+
// Parsers
|
|
5
|
+
export { parseSESNotification, parseSNSMessage } from './notification-parser.js';
|
|
6
|
+
// Middleware
|
|
7
|
+
export { clearCertCache, verifySNSSignature, } from './signature-verification.js';
|
|
8
|
+
export { handleSNSSubscription, } from './subscription-handler.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/email/webhooks/index.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAEhB,kBAAkB;AAClB,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,UAAU;AACV,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACjF,aAAa;AACb,OAAO,EACL,cAAc,EACd,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEL,qBAAqB,GACtB,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { SESEvent, SNSMessage } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses an SES notification from an SNS message.
|
|
5
|
+
*
|
|
6
|
+
* Returns null if the message is not a notification (e.g., subscription confirmation)
|
|
7
|
+
* or if it's not an SES event we handle.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { parseSESNotification, verifySNSSignature, handleSNSSubscription } from 'glasswork';
|
|
12
|
+
*
|
|
13
|
+
* router.post('/webhooks/ses',
|
|
14
|
+
* verifySNSSignature(),
|
|
15
|
+
* handleSNSSubscription(),
|
|
16
|
+
* async (c) => {
|
|
17
|
+
* const event = await parseSESNotification(c);
|
|
18
|
+
*
|
|
19
|
+
* if (!event) {
|
|
20
|
+
* return c.json({ received: true });
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* switch (event.type) {
|
|
24
|
+
* case 'delivered':
|
|
25
|
+
* console.log('Email delivered:', event.messageId);
|
|
26
|
+
* break;
|
|
27
|
+
* case 'bounced':
|
|
28
|
+
* console.log('Email bounced:', event.messageId, event.bounceType);
|
|
29
|
+
* break;
|
|
30
|
+
* case 'complaint':
|
|
31
|
+
* console.log('Complaint received:', event.messageId);
|
|
32
|
+
* break;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* return c.json({ received: true });
|
|
36
|
+
* }
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseSESNotification(c: Context): Promise<SESEvent | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Lower-level function to parse just the SNS message
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSNSMessage(c: Context): Promise<SNSMessage>;
|
|
45
|
+
//# sourceMappingURL=notification-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-parser.d.ts","sourceRoot":"","sources":["../../../src/email/webhooks/notification-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAOV,QAAQ,EACR,UAAU,EACX,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA8C/E;AAgED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAWrE"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createLogger } from '../../utils/logger.js';
|
|
2
|
+
const logger = createLogger('SES');
|
|
3
|
+
/**
|
|
4
|
+
* Parses an SES notification from an SNS message.
|
|
5
|
+
*
|
|
6
|
+
* Returns null if the message is not a notification (e.g., subscription confirmation)
|
|
7
|
+
* or if it's not an SES event we handle.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { parseSESNotification, verifySNSSignature, handleSNSSubscription } from 'glasswork';
|
|
12
|
+
*
|
|
13
|
+
* router.post('/webhooks/ses',
|
|
14
|
+
* verifySNSSignature(),
|
|
15
|
+
* handleSNSSubscription(),
|
|
16
|
+
* async (c) => {
|
|
17
|
+
* const event = await parseSESNotification(c);
|
|
18
|
+
*
|
|
19
|
+
* if (!event) {
|
|
20
|
+
* return c.json({ received: true });
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* switch (event.type) {
|
|
24
|
+
* case 'delivered':
|
|
25
|
+
* console.log('Email delivered:', event.messageId);
|
|
26
|
+
* break;
|
|
27
|
+
* case 'bounced':
|
|
28
|
+
* console.log('Email bounced:', event.messageId, event.bounceType);
|
|
29
|
+
* break;
|
|
30
|
+
* case 'complaint':
|
|
31
|
+
* console.log('Complaint received:', event.messageId);
|
|
32
|
+
* break;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* return c.json({ received: true });
|
|
36
|
+
* }
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function parseSESNotification(c) {
|
|
41
|
+
// Try to get the pre-parsed message from context
|
|
42
|
+
let snsMessage = c.get('snsMessage');
|
|
43
|
+
if (!snsMessage) {
|
|
44
|
+
// Parse from body
|
|
45
|
+
try {
|
|
46
|
+
const rawBody = c.get('snsMessageRaw');
|
|
47
|
+
const body = rawBody || (await c.req.text());
|
|
48
|
+
snsMessage = JSON.parse(body);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Only handle notifications
|
|
55
|
+
if (snsMessage.Type !== 'Notification') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Parse the inner SES notification
|
|
59
|
+
let notification;
|
|
60
|
+
try {
|
|
61
|
+
notification = JSON.parse(snsMessage.Message);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
logger.error('Failed to parse notification message');
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// Route to the appropriate parser based on notification type
|
|
68
|
+
// SES can use either notificationType (older) or eventType (newer format)
|
|
69
|
+
const eventType = notification.notificationType ||
|
|
70
|
+
notification.eventType;
|
|
71
|
+
switch (eventType) {
|
|
72
|
+
case 'Delivery':
|
|
73
|
+
return parseDeliveryNotification(notification);
|
|
74
|
+
case 'Bounce':
|
|
75
|
+
return parseBounceNotification(notification);
|
|
76
|
+
case 'Complaint':
|
|
77
|
+
return parseComplaintNotification(notification);
|
|
78
|
+
default:
|
|
79
|
+
// Send event is ignored (we track via onSent hook)
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parses a delivery notification into a DeliveredEvent
|
|
85
|
+
*/
|
|
86
|
+
function parseDeliveryNotification(notification) {
|
|
87
|
+
const { mail, delivery } = notification;
|
|
88
|
+
const recipient = delivery.recipients[0] || mail.destination[0];
|
|
89
|
+
return {
|
|
90
|
+
type: 'delivered',
|
|
91
|
+
messageId: mail.messageId,
|
|
92
|
+
timestamp: new Date(delivery.timestamp),
|
|
93
|
+
recipient,
|
|
94
|
+
processingTimeMs: delivery.processingTimeMillis,
|
|
95
|
+
smtpResponse: delivery.smtpResponse,
|
|
96
|
+
raw: notification,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parses a bounce notification into a BouncedEvent
|
|
101
|
+
*/
|
|
102
|
+
function parseBounceNotification(notification) {
|
|
103
|
+
const { mail, bounce } = notification;
|
|
104
|
+
const recipient = bounce.bouncedRecipients[0]?.emailAddress || mail.destination[0];
|
|
105
|
+
const bounceType = bounce.bounceType === 'Permanent' ? 'permanent' : 'transient';
|
|
106
|
+
// Build a human-readable reason
|
|
107
|
+
const diagnosticCode = bounce.bouncedRecipients[0]?.diagnosticCode || 'Unknown reason';
|
|
108
|
+
const reason = `${bounce.bounceSubType}: ${diagnosticCode}`;
|
|
109
|
+
return {
|
|
110
|
+
type: 'bounced',
|
|
111
|
+
messageId: mail.messageId,
|
|
112
|
+
timestamp: new Date(bounce.timestamp),
|
|
113
|
+
recipient,
|
|
114
|
+
bounceType,
|
|
115
|
+
bounceSubType: bounce.bounceSubType,
|
|
116
|
+
reason,
|
|
117
|
+
raw: notification,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Parses a complaint notification into a ComplaintEvent
|
|
122
|
+
*/
|
|
123
|
+
function parseComplaintNotification(notification) {
|
|
124
|
+
const { mail, complaint } = notification;
|
|
125
|
+
const recipient = complaint.complainedRecipients[0]?.emailAddress || mail.destination[0];
|
|
126
|
+
return {
|
|
127
|
+
type: 'complaint',
|
|
128
|
+
messageId: mail.messageId,
|
|
129
|
+
timestamp: new Date(complaint.timestamp),
|
|
130
|
+
recipient,
|
|
131
|
+
complaintType: complaint.complaintFeedbackType || 'unknown',
|
|
132
|
+
userAgent: complaint.userAgent,
|
|
133
|
+
raw: notification,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Lower-level function to parse just the SNS message
|
|
138
|
+
*/
|
|
139
|
+
export async function parseSNSMessage(c) {
|
|
140
|
+
// Try to get from context first
|
|
141
|
+
const cached = c.get('snsMessage');
|
|
142
|
+
if (cached) {
|
|
143
|
+
return cached;
|
|
144
|
+
}
|
|
145
|
+
// Parse from body
|
|
146
|
+
const rawBody = c.get('snsMessageRaw');
|
|
147
|
+
const body = rawBody || (await c.req.text());
|
|
148
|
+
return JSON.parse(body);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=notification-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-parser.js","sourceRoot":"","sources":["../../../src/email/webhooks/notification-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { SNSMessage, VerifySignatureOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the canonical string to sign for SNS message verification
|
|
5
|
+
*
|
|
6
|
+
* Ordering differs per message type:
|
|
7
|
+
* - Notification: Message, MessageId, optional Subject, Timestamp, TopicArn, Type
|
|
8
|
+
* - SubscriptionConfirmation/UnsubscribeConfirmation: Message, MessageId, SubscribeURL, Timestamp, Token, TopicArn, Type
|
|
9
|
+
*
|
|
10
|
+
* See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildStringToSign(message: SNSMessage): string;
|
|
13
|
+
/**
|
|
14
|
+
* Middleware that verifies AWS SNS message signatures.
|
|
15
|
+
*
|
|
16
|
+
* Protects webhook endpoints from spoofed requests by validating
|
|
17
|
+
* that messages are genuinely from AWS SNS.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { verifySNSSignature } from 'glasswork';
|
|
22
|
+
*
|
|
23
|
+
* router.post('/webhooks/ses',
|
|
24
|
+
* verifySNSSignature(),
|
|
25
|
+
* async (c) => {
|
|
26
|
+
* // Message is verified to be from AWS SNS
|
|
27
|
+
* const body = await c.req.json();
|
|
28
|
+
* // ...
|
|
29
|
+
* }
|
|
30
|
+
* );
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifySNSSignature(options?: VerifySignatureOptions): MiddlewareHandler;
|
|
34
|
+
/**
|
|
35
|
+
* Clears the certificate cache (useful for testing)
|
|
36
|
+
*/
|
|
37
|
+
export declare function clearCertCache(): void;
|
|
38
|
+
//# sourceMappingURL=signature-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature-verification.d.ts","sourceRoot":"","sources":["../../../src/email/webhooks/signature-verification.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAgErE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAmC7D;AAqBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,sBAA2B,GAAG,iBAAiB,CA6C1F;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|