@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.
Files changed (61) hide show
  1. package/basic/demo.ts +11 -0
  2. package/basic/index.ts +490 -0
  3. package/core/Auth.ts +63 -0
  4. package/core/Currency.ts +16 -0
  5. package/core/Env.ts +186 -0
  6. package/core/Fragment.ts +43 -0
  7. package/core/FragmentServingMode.ts +37 -0
  8. package/core/JsonSchemaValidator.ts +173 -0
  9. package/core/ProjectRoot.ts +76 -0
  10. package/core/Scratch.ts +44 -0
  11. package/core/URI.ts +203 -0
  12. package/core/Universe.ts +406 -0
  13. package/core/index.ts +27 -0
  14. package/gsuite/core/GSuite.ts +237 -0
  15. package/gsuite/core/GSuiteAdmin.ts +81 -0
  16. package/gsuite/core/GSuiteOrgConfig.ts +47 -0
  17. package/gsuite/core/GSuiteUser.ts +115 -0
  18. package/gsuite/core/index.ts +21 -0
  19. package/gsuite/documents/Document.ts +173 -0
  20. package/gsuite/documents/Documents.ts +52 -0
  21. package/gsuite/documents/index.ts +19 -0
  22. package/gsuite/drive/Drive.ts +118 -0
  23. package/gsuite/drive/DriveFile.ts +147 -0
  24. package/gsuite/drive/index.ts +19 -0
  25. package/gsuite/gmail/Gmail.ts +430 -0
  26. package/gsuite/gmail/GmailLabel.ts +55 -0
  27. package/gsuite/gmail/GmailMessage.ts +428 -0
  28. package/gsuite/gmail/GmailMessagePart.ts +298 -0
  29. package/gsuite/gmail/GmailThread.ts +97 -0
  30. package/gsuite/gmail/index.ts +5 -0
  31. package/gsuite/gmail/utils.ts +184 -0
  32. package/gsuite/index.ts +28 -0
  33. package/gsuite/spreadsheets/CellValue.ts +71 -0
  34. package/gsuite/spreadsheets/Sheet.ts +128 -0
  35. package/gsuite/spreadsheets/SheetValues.ts +12 -0
  36. package/gsuite/spreadsheets/Spreadsheet.ts +76 -0
  37. package/gsuite/spreadsheets/Spreadsheets.ts +52 -0
  38. package/gsuite/spreadsheets/index.ts +25 -0
  39. package/gsuite/spreadsheets/utils.ts +52 -0
  40. package/gsuite/utils.ts +104 -0
  41. package/http-server/HttpServer.ts +110 -0
  42. package/http-server/NativeHttpServer.ts +1084 -0
  43. package/http-server/index.ts +3 -0
  44. package/http-server/middlewares/01-cors.ts +33 -0
  45. package/http-server/middlewares/02-static.ts +67 -0
  46. package/http-server/middlewares/03-request-logger.ts +159 -0
  47. package/http-server/middlewares/04-body-parser.ts +54 -0
  48. package/http-server/middlewares/05-no-cache.ts +23 -0
  49. package/http-server/middlewares/06-response-handler.ts +39 -0
  50. package/http-server/middlewares/handler-wrapper.ts +250 -0
  51. package/http-server/middlewares/index.ts +37 -0
  52. package/http-server/middlewares/types.ts +27 -0
  53. package/index.ts +24 -0
  54. package/package.json +37 -0
  55. package/queue/EmailQueue.ts +228 -0
  56. package/queue/RateLimiter.ts +54 -0
  57. package/queue/index.ts +2 -0
  58. package/resend/Resend.ts +190 -0
  59. package/resend/index.ts +11 -0
  60. package/s2/S2.ts +335 -0
  61. 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
@@ -0,0 +1,2 @@
1
+ export * from "./EmailQueue";
2
+ export * from "./RateLimiter";
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resend Module - Email Service Adapter
3
+ *
4
+ * This module provides an adapter for the Resend email service API.
5
+ *
6
+ * @module Resend
7
+ * @version 1.0.0
8
+ * @author Divizend GmbH
9
+ */
10
+
11
+ export * from "./Resend";