@divizend/scratch-core 1.0.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/basic/demo.ts +11 -0
- package/basic/index.ts +490 -0
- package/core/Auth.ts +63 -0
- package/core/Currency.ts +16 -0
- package/core/Env.ts +186 -0
- package/core/Fragment.ts +43 -0
- package/core/FragmentServingMode.ts +37 -0
- package/core/JsonSchemaValidator.ts +173 -0
- package/core/ProjectRoot.ts +76 -0
- package/core/Scratch.ts +44 -0
- package/core/URI.ts +203 -0
- package/core/Universe.ts +406 -0
- package/core/index.ts +27 -0
- package/gsuite/core/GSuite.ts +237 -0
- package/gsuite/core/GSuiteAdmin.ts +81 -0
- package/gsuite/core/GSuiteOrgConfig.ts +47 -0
- package/gsuite/core/GSuiteUser.ts +115 -0
- package/gsuite/core/index.ts +21 -0
- package/gsuite/documents/Document.ts +173 -0
- package/gsuite/documents/Documents.ts +52 -0
- package/gsuite/documents/index.ts +19 -0
- package/gsuite/drive/Drive.ts +118 -0
- package/gsuite/drive/DriveFile.ts +147 -0
- package/gsuite/drive/index.ts +19 -0
- package/gsuite/gmail/Gmail.ts +430 -0
- package/gsuite/gmail/GmailLabel.ts +55 -0
- package/gsuite/gmail/GmailMessage.ts +428 -0
- package/gsuite/gmail/GmailMessagePart.ts +298 -0
- package/gsuite/gmail/GmailThread.ts +97 -0
- package/gsuite/gmail/index.ts +5 -0
- package/gsuite/gmail/utils.ts +184 -0
- package/gsuite/index.ts +28 -0
- package/gsuite/spreadsheets/CellValue.ts +71 -0
- package/gsuite/spreadsheets/Sheet.ts +128 -0
- package/gsuite/spreadsheets/SheetValues.ts +12 -0
- package/gsuite/spreadsheets/Spreadsheet.ts +76 -0
- package/gsuite/spreadsheets/Spreadsheets.ts +52 -0
- package/gsuite/spreadsheets/index.ts +25 -0
- package/gsuite/spreadsheets/utils.ts +52 -0
- package/gsuite/utils.ts +104 -0
- package/http-server/HttpServer.ts +110 -0
- package/http-server/NativeHttpServer.ts +1084 -0
- package/http-server/index.ts +3 -0
- package/http-server/middlewares/01-cors.ts +33 -0
- package/http-server/middlewares/02-static.ts +67 -0
- package/http-server/middlewares/03-request-logger.ts +159 -0
- package/http-server/middlewares/04-body-parser.ts +54 -0
- package/http-server/middlewares/05-no-cache.ts +23 -0
- package/http-server/middlewares/06-response-handler.ts +39 -0
- package/http-server/middlewares/handler-wrapper.ts +250 -0
- package/http-server/middlewares/index.ts +37 -0
- package/http-server/middlewares/types.ts +27 -0
- package/index.ts +24 -0
- package/package.json +37 -0
- package/queue/EmailQueue.ts +228 -0
- package/queue/RateLimiter.ts +54 -0
- package/queue/index.ts +2 -0
- package/resend/Resend.ts +190 -0
- package/resend/index.ts +11 -0
- package/s2/S2.ts +335 -0
- package/s2/index.ts +11 -0
package/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Executive - Intelligent Email Management and Processing System
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the AI Executive system, which provides:
|
|
5
|
+
* - Gmail integration with advanced email processing capabilities
|
|
6
|
+
* - AI-powered email analysis and response generation
|
|
7
|
+
* - Workflow automation for business processes
|
|
8
|
+
* - Database management for email fragments and metadata
|
|
9
|
+
*
|
|
10
|
+
* The system is designed to handle enterprise-scale email management
|
|
11
|
+
* with intelligent automation and AI assistance.
|
|
12
|
+
*
|
|
13
|
+
* @module AI Executive
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
* @author Divizend GmbH
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export * from "./basic";
|
|
19
|
+
export * from "./core";
|
|
20
|
+
export * from "./gsuite";
|
|
21
|
+
export * from "./http-server";
|
|
22
|
+
export * from "./resend";
|
|
23
|
+
export * from "./s2";
|
|
24
|
+
export * from "./queue";
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@divizend/scratch-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core library for Scratch endpoint system",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"**/*.ts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prepublishOnly": "echo 'Publishing @divizend/scratch-core'"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@s2-dev/streamstore": "^0.18.1",
|
|
18
|
+
"cloudevents": "^10.0.0",
|
|
19
|
+
"google-auth-library": "^10.5.0",
|
|
20
|
+
"googleapis": "^167.0.0",
|
|
21
|
+
"jose": "^5.6.0",
|
|
22
|
+
"jsonpath-plus": "^10.3.0",
|
|
23
|
+
"marked": "^12.0.0",
|
|
24
|
+
"mustache": "^4.2.0",
|
|
25
|
+
"turndown": "^7.2.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "^1.3.3",
|
|
29
|
+
"@types/mustache": "^4.2.6",
|
|
30
|
+
"@types/node": "^24.10.1",
|
|
31
|
+
"@types/turndown": "^5.0.6",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { RateLimiter } from "./RateLimiter";
|
|
2
|
+
|
|
3
|
+
export interface QueuedEmail {
|
|
4
|
+
id: string;
|
|
5
|
+
from: string;
|
|
6
|
+
to: string;
|
|
7
|
+
subject: string;
|
|
8
|
+
content: string;
|
|
9
|
+
queuedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SendResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
sent: number;
|
|
15
|
+
errors: number;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EmailProfile {
|
|
20
|
+
/** Array of domains this profile handles */
|
|
21
|
+
domains: string[];
|
|
22
|
+
/** Function to send an email using this profile */
|
|
23
|
+
sendHandler: (email: QueuedEmail) => Promise<void>;
|
|
24
|
+
/** Optional function to get domains dynamically (for validation) */
|
|
25
|
+
getDomains?: () => Promise<string[]> | string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class EmailQueue {
|
|
29
|
+
private queue: QueuedEmail[] = [];
|
|
30
|
+
private isSending = false;
|
|
31
|
+
private rateLimiter: RateLimiter;
|
|
32
|
+
private profiles: EmailProfile[];
|
|
33
|
+
|
|
34
|
+
constructor(profiles: EmailProfile[], rateLimitDelayMs: number = 100) {
|
|
35
|
+
this.profiles = profiles;
|
|
36
|
+
this.rateLimiter = new RateLimiter(rateLimitDelayMs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add an email to the queue
|
|
41
|
+
*/
|
|
42
|
+
add(email: Omit<QueuedEmail, "id" | "queuedAt">): QueuedEmail {
|
|
43
|
+
const queuedEmail: QueuedEmail = {
|
|
44
|
+
...email,
|
|
45
|
+
id: crypto.randomUUID(),
|
|
46
|
+
queuedAt: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
this.queue.push(queuedEmail);
|
|
49
|
+
return queuedEmail;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all emails in the queue
|
|
54
|
+
*/
|
|
55
|
+
getAll(): QueuedEmail[] {
|
|
56
|
+
return [...this.queue];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get emails by IDs
|
|
61
|
+
*/
|
|
62
|
+
getByIds(ids: string[]): QueuedEmail[] {
|
|
63
|
+
return this.queue.filter((email) => ids.includes(email.id));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clear all emails from the queue
|
|
68
|
+
*/
|
|
69
|
+
clear(): void {
|
|
70
|
+
this.queue.length = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove emails by IDs
|
|
75
|
+
*/
|
|
76
|
+
removeByIds(ids: string[]): number {
|
|
77
|
+
const initialLength = this.queue.length;
|
|
78
|
+
this.queue = this.queue.filter((email) => !ids.includes(email.id));
|
|
79
|
+
return initialLength - this.queue.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if emails are currently being sent
|
|
84
|
+
*/
|
|
85
|
+
getIsSending(): boolean {
|
|
86
|
+
return this.isSending;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all domains from all profiles
|
|
91
|
+
*/
|
|
92
|
+
async getAllDomains(): Promise<string[]> {
|
|
93
|
+
const allDomains: string[] = [];
|
|
94
|
+
|
|
95
|
+
for (const profile of this.profiles) {
|
|
96
|
+
if (profile.getDomains) {
|
|
97
|
+
const domains = await profile.getDomains();
|
|
98
|
+
allDomains.push(
|
|
99
|
+
...(Array.isArray(domains) ? domains : await Promise.resolve(domains))
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
allDomains.push(...profile.domains);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [...new Set(allDomains)]; // Remove duplicates
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find the profile that handles a given domain
|
|
111
|
+
*/
|
|
112
|
+
private async findProfileForDomain(
|
|
113
|
+
domain: string
|
|
114
|
+
): Promise<EmailProfile | null> {
|
|
115
|
+
for (const profile of this.profiles) {
|
|
116
|
+
let profileDomains: string[] = [];
|
|
117
|
+
|
|
118
|
+
if (profile.getDomains) {
|
|
119
|
+
const domains = await profile.getDomains();
|
|
120
|
+
profileDomains = Array.isArray(domains)
|
|
121
|
+
? domains
|
|
122
|
+
: await Promise.resolve(domains);
|
|
123
|
+
} else {
|
|
124
|
+
profileDomains = profile.domains;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (profileDomains.includes(domain)) {
|
|
128
|
+
return profile;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate that a domain is handled by one of the profiles
|
|
137
|
+
*/
|
|
138
|
+
async validateDomain(domain: string): Promise<boolean> {
|
|
139
|
+
const profile = await this.findProfileForDomain(domain);
|
|
140
|
+
return profile !== null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Send emails (all or selected by IDs)
|
|
145
|
+
* Routes emails to appropriate profile handlers based on sender domain
|
|
146
|
+
* @param ids - Array of email IDs to send, or null to send all
|
|
147
|
+
* @returns Send result with statistics
|
|
148
|
+
*/
|
|
149
|
+
async send(ids: string[] | null): Promise<SendResult> {
|
|
150
|
+
if (this.isSending) {
|
|
151
|
+
throw new Error("Email sending already in progress");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Always create a copy to avoid modifying the queue while iterating
|
|
155
|
+
const emailsToSend =
|
|
156
|
+
ids === null
|
|
157
|
+
? [...this.queue] // Copy all emails
|
|
158
|
+
: this.getByIds(ids); // getByIds already returns a filtered copy
|
|
159
|
+
|
|
160
|
+
if (emailsToSend.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
sent: 0,
|
|
164
|
+
errors: 0,
|
|
165
|
+
message: "No emails to send",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.isSending = true;
|
|
170
|
+
let sent = 0;
|
|
171
|
+
let errors = 0;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Send emails with rate limiting
|
|
175
|
+
// emailsToSend is already a copy, so safe to iterate
|
|
176
|
+
await this.rateLimiter.process(emailsToSend, async (email) => {
|
|
177
|
+
try {
|
|
178
|
+
// Extract domain from sender email
|
|
179
|
+
const fromDomain = email.from.split("@")[1];
|
|
180
|
+
if (!fromDomain) {
|
|
181
|
+
throw new Error(`Invalid sender email: ${email.from}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Find the profile that handles this domain
|
|
185
|
+
const profile = await this.findProfileForDomain(fromDomain);
|
|
186
|
+
|
|
187
|
+
if (!profile) {
|
|
188
|
+
// This should not happen since domain was validated when queuing
|
|
189
|
+
// But handle it gracefully just in case
|
|
190
|
+
errors++;
|
|
191
|
+
console.error(
|
|
192
|
+
`Unexpected: Unrecognized sender domain: ${fromDomain} for queued email ${email.id}. This should have been caught during validation.`
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Send using the profile's handler
|
|
198
|
+
await profile.sendHandler(email);
|
|
199
|
+
|
|
200
|
+
sent++;
|
|
201
|
+
// Remove sent email from queue
|
|
202
|
+
const index = this.queue.findIndex((e) => e.id === email.id);
|
|
203
|
+
if (index !== -1) {
|
|
204
|
+
this.queue.splice(index, 1);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
errors++;
|
|
208
|
+
console.error(
|
|
209
|
+
`Error processing email ${email.id}: ${
|
|
210
|
+
error instanceof Error ? error.message : String(error)
|
|
211
|
+
}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
this.isSending = false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
sent,
|
|
222
|
+
errors,
|
|
223
|
+
message: `Sent ${sent} email(s)${
|
|
224
|
+
errors > 0 ? `, ${errors} error(s)` : ""
|
|
225
|
+
}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter utility for controlling the rate of operations
|
|
3
|
+
*/
|
|
4
|
+
export class RateLimiter {
|
|
5
|
+
private delayMs: number;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new rate limiter
|
|
9
|
+
* @param delayMs - Delay in milliseconds between operations
|
|
10
|
+
*/
|
|
11
|
+
constructor(delayMs: number) {
|
|
12
|
+
this.delayMs = delayMs;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wait for the configured delay period
|
|
17
|
+
* @returns Promise that resolves after the delay
|
|
18
|
+
*/
|
|
19
|
+
async wait(): Promise<void> {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, this.delayMs));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the delay in milliseconds
|
|
25
|
+
*/
|
|
26
|
+
getDelay(): number {
|
|
27
|
+
return this.delayMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Process items with rate limiting
|
|
32
|
+
* @param items - Array of items to process
|
|
33
|
+
* @param processor - Async function to process each item
|
|
34
|
+
* @returns Array of results from processing each item
|
|
35
|
+
*/
|
|
36
|
+
async process<T, R>(
|
|
37
|
+
items: T[],
|
|
38
|
+
processor: (item: T, index: number) => Promise<R>
|
|
39
|
+
): Promise<R[]> {
|
|
40
|
+
const results: R[] = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < items.length; i++) {
|
|
43
|
+
const result = await processor(items[i], i);
|
|
44
|
+
results.push(result);
|
|
45
|
+
|
|
46
|
+
// Wait before processing next item (except for the last one)
|
|
47
|
+
if (i < items.length - 1) {
|
|
48
|
+
await this.wait();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/queue/index.ts
ADDED
package/resend/Resend.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend - Email Service Adapter
|
|
3
|
+
*
|
|
4
|
+
* The Resend class provides an adapter for the Resend email service API,
|
|
5
|
+
* abstracting the HTTP calls and configuration.
|
|
6
|
+
*
|
|
7
|
+
* @class Resend
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { envOr, envOrDefault } from "../index";
|
|
12
|
+
|
|
13
|
+
export interface ResendEmailParams {
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
html: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResendResponse {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResendDomain {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
status: string;
|
|
30
|
+
created_at: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Resend {
|
|
34
|
+
private apiKey: string;
|
|
35
|
+
private apiRoot: string;
|
|
36
|
+
private cachedDomains: string[] = [];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Private constructor - use Resend.construct() instead
|
|
40
|
+
*
|
|
41
|
+
* @param apiKey - Resend API key
|
|
42
|
+
* @param apiRoot - Resend API root URL
|
|
43
|
+
* @param domains - Pre-fetched domains array
|
|
44
|
+
*/
|
|
45
|
+
private constructor(apiKey: string, apiRoot: string, domains: string[] = []) {
|
|
46
|
+
this.apiKey = apiKey;
|
|
47
|
+
this.apiRoot = apiRoot;
|
|
48
|
+
this.cachedDomains = domains;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a new Resend instance and fetches domains once
|
|
53
|
+
* Automatically reads API key from RESEND_API_KEY environment variable
|
|
54
|
+
* and API root from RESEND_API_ROOT (defaults to "api.resend.com")
|
|
55
|
+
*
|
|
56
|
+
* @param apiKey - Optional Resend API key (overrides RESEND_API_KEY env var)
|
|
57
|
+
* @param apiRoot - Optional Resend API root URL (overrides RESEND_API_ROOT env var, defaults to "api.resend.com")
|
|
58
|
+
* @returns Promise<Resend> - Resend instance with domains cached
|
|
59
|
+
* @throws Error if API key is not provided and RESEND_API_KEY is not set
|
|
60
|
+
*/
|
|
61
|
+
static async construct(apiKey?: string, apiRoot?: string): Promise<Resend> {
|
|
62
|
+
const key = envOr(
|
|
63
|
+
apiKey,
|
|
64
|
+
"RESEND_API_KEY",
|
|
65
|
+
"Resend API key is required. Provide it via parameter or RESEND_API_KEY environment variable."
|
|
66
|
+
);
|
|
67
|
+
const root = envOrDefault(apiRoot, "RESEND_API_ROOT", "api.resend.com");
|
|
68
|
+
|
|
69
|
+
// Fetch domains once during construction
|
|
70
|
+
let domains: string[] = [];
|
|
71
|
+
try {
|
|
72
|
+
domains = await Resend.fetchDomains(key, root);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn(
|
|
75
|
+
"Failed to fetch Resend domains during construction:",
|
|
76
|
+
error
|
|
77
|
+
);
|
|
78
|
+
// Continue with empty array - domains can be empty if fetch fails
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Resend(key, root, domains);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetches domains from Resend API
|
|
86
|
+
* Called once during construction
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
* @static
|
|
90
|
+
* @param apiKey - Resend API key
|
|
91
|
+
* @param apiRoot - Resend API root URL
|
|
92
|
+
* @returns Promise<string[]> - Array of domain names
|
|
93
|
+
*/
|
|
94
|
+
private static async fetchDomains(
|
|
95
|
+
apiKey: string,
|
|
96
|
+
apiRoot: string
|
|
97
|
+
): Promise<string[]> {
|
|
98
|
+
const url = `https://${apiRoot}/domains`;
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: "GET",
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${apiKey}`,
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
throw new Error(`Failed to fetch Resend domains: ${text}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = (await response.json()) as { data?: ResendDomain[] };
|
|
113
|
+
const domains: ResendDomain[] = data.data || [];
|
|
114
|
+
|
|
115
|
+
// Extract domain names and filter by verified status
|
|
116
|
+
return domains
|
|
117
|
+
.filter((domain) => domain.status === "verified")
|
|
118
|
+
.map((domain) => domain.name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gets all cached domains from Resend
|
|
123
|
+
* Returns the domains that were fetched once during construction
|
|
124
|
+
*
|
|
125
|
+
* @returns string[] - Array of domain names (cached, synchronous)
|
|
126
|
+
*/
|
|
127
|
+
getDomains(): string[] {
|
|
128
|
+
return this.cachedDomains;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sends an email via Resend API
|
|
133
|
+
*
|
|
134
|
+
* @param params - Email parameters
|
|
135
|
+
* @returns Promise<ResendResponse> - Response from Resend API
|
|
136
|
+
*/
|
|
137
|
+
async sendEmail(params: ResendEmailParams): Promise<ResendResponse> {
|
|
138
|
+
const url = `https://${this.apiRoot}/emails`;
|
|
139
|
+
const response = await fetch(url, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
from: params.from,
|
|
147
|
+
to: [params.to],
|
|
148
|
+
subject: params.subject,
|
|
149
|
+
html: params.html,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const text = await response.text();
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ok: response.ok,
|
|
157
|
+
status: response.status,
|
|
158
|
+
text,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks the health of the Resend service
|
|
164
|
+
* Verifies connectivity by checking cached domains
|
|
165
|
+
*
|
|
166
|
+
* @returns { status: string; message: string; connected: boolean; domains?: number }
|
|
167
|
+
*/
|
|
168
|
+
getHealth(): {
|
|
169
|
+
status: string;
|
|
170
|
+
message: string;
|
|
171
|
+
connected: boolean;
|
|
172
|
+
domains?: number;
|
|
173
|
+
} {
|
|
174
|
+
try {
|
|
175
|
+
const domains = this.getDomains();
|
|
176
|
+
return {
|
|
177
|
+
status: "ok",
|
|
178
|
+
message: "Resend connected",
|
|
179
|
+
connected: true,
|
|
180
|
+
domains: domains.length,
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
status: "error",
|
|
185
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
186
|
+
connected: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|