@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/core/Env.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Environment Variable Utility
3
+ *
4
+ * Provides a centralized way to access and validate environment variables
5
+ * with consistent error messages and validation logic.
6
+ *
7
+ * @module Env
8
+ */
9
+
10
+ /**
11
+ * Gets an environment variable with optional validation
12
+ *
13
+ * @param name - The name of the environment variable
14
+ * @param options - Optional configuration
15
+ * @param options.required - If true, throws an error if the variable is not set (default: true)
16
+ * @param options.defaultValue - Default value if the variable is not set (only used if required is false)
17
+ * @param options.errorMessage - Custom error message if the variable is required but not set
18
+ * @returns The environment variable value
19
+ * @throws Error if the variable is required but not set
20
+ */
21
+ export function env(
22
+ name: string,
23
+ options?: {
24
+ required?: boolean;
25
+ defaultValue?: string;
26
+ errorMessage?: string;
27
+ }
28
+ ): string {
29
+ const value = process.env[name];
30
+ const required = options?.required !== false; // Default to true
31
+
32
+ if (!value) {
33
+ if (required) {
34
+ const errorMessage =
35
+ options?.errorMessage ||
36
+ `${name} environment variable is required but not set`;
37
+ throw new Error(errorMessage);
38
+ }
39
+ return options?.defaultValue || "";
40
+ }
41
+
42
+ return value;
43
+ }
44
+
45
+ /**
46
+ * Gets a value from a parameter or falls back to an environment variable
47
+ * Throws an error if neither is provided
48
+ *
49
+ * @param param - The parameter value (can be undefined)
50
+ * @param envVarName - The name of the environment variable to use as fallback
51
+ * @param errorMessage - Error message to throw if neither param nor env var is set
52
+ * @returns The parameter value or the environment variable value
53
+ * @throws Error if both param and env var are not set
54
+ *
55
+ * @example
56
+ * const apiKey = envOr(providedKey, "API_KEY", "API key is required");
57
+ */
58
+ export function envOr(
59
+ param: string | undefined,
60
+ envVarName: string,
61
+ errorMessage: string
62
+ ): string {
63
+ if (param) {
64
+ return param;
65
+ }
66
+ const envValue = process.env[envVarName];
67
+ if (!envValue) {
68
+ throw new Error(errorMessage);
69
+ }
70
+ return envValue;
71
+ }
72
+
73
+ /**
74
+ * Gets a value from a parameter or falls back to an environment variable with a default
75
+ * Returns the default if neither is provided
76
+ *
77
+ * @param param - The parameter value (can be undefined)
78
+ * @param envVarName - The name of the environment variable to use as fallback
79
+ * @param defaultValue - Default value if neither param nor env var is set
80
+ * @returns The parameter value, environment variable value, or default value
81
+ *
82
+ * @example
83
+ * const port = envOrDefault(providedPort, "PORT", "3000");
84
+ */
85
+ export function envOrDefault(
86
+ param: string | undefined,
87
+ envVarName: string,
88
+ defaultValue: string
89
+ ): string {
90
+ if (param) {
91
+ return param;
92
+ }
93
+ return process.env[envVarName] || defaultValue;
94
+ }
95
+
96
+ /**
97
+ * Parses environment variables grouped by a common prefix pattern
98
+ *
99
+ * Scans all environment variables and groups them by identifier extracted from the prefix.
100
+ * Useful for parsing variables like GCP_CLIENT_EMAIL_ORG1, GCP_PRIVATE_KEY_ORG1, etc.
101
+ *
102
+ * @param prefixes - Array of prefix patterns to match (e.g., ["GCP_CLIENT_EMAIL_", "GCP_PRIVATE_KEY_"])
103
+ * @param options - Configuration options
104
+ * @param options.propertyMap - Maps each prefix to a property name in the result object
105
+ * @param options.identifierExtractor - Function to extract identifier from env var name (default: splits by "_" and takes 4th segment, lowercased)
106
+ * @param options.required - If true, throws error if no matching env vars found (default: true)
107
+ * @param options.errorMessage - Custom error message if no env vars found
108
+ * @returns Object grouped by identifier, each containing properties mapped from prefixes
109
+ * @throws Error if required is true and no matching env vars are found
110
+ *
111
+ * @example
112
+ * // For env vars: GCP_CLIENT_EMAIL_ORG1=..., GCP_PRIVATE_KEY_ORG1=..., GCP_ADMIN_USER_ORG1=...
113
+ * const creds = parseEnvGroup(
114
+ * ["GCP_CLIENT_EMAIL_", "GCP_PRIVATE_KEY_", "GCP_ADMIN_USER_"],
115
+ * {
116
+ * propertyMap: {
117
+ * "GCP_CLIENT_EMAIL_": "clientEmail",
118
+ * "GCP_PRIVATE_KEY_": "privateKey",
119
+ * "GCP_ADMIN_USER_": "adminUser",
120
+ * }
121
+ * }
122
+ * );
123
+ * // Returns: { org1: { clientEmail: "...", privateKey: "...", adminUser: "..." } }
124
+ */
125
+ export function parseEnvGroup<T extends Record<string, string>>(
126
+ prefixes: string[],
127
+ options: {
128
+ propertyMap: { [prefix: string]: keyof T };
129
+ identifierExtractor?: (key: string, prefix: string) => string;
130
+ required?: boolean;
131
+ errorMessage?: string;
132
+ }
133
+ ): { [identifier: string]: Partial<T> } {
134
+ const result: { [identifier: string]: Partial<T> } = {};
135
+ const identifierExtractor =
136
+ options.identifierExtractor ||
137
+ ((key: string, prefix: string) => {
138
+ // Default: split by "_" and take the part after the prefix
139
+ // For "GCP_CLIENT_EMAIL_ORG1", prefix is "GCP_CLIENT_EMAIL_", identifier is "org1"
140
+ const parts = key.split("_");
141
+ // Find which prefix matched and get the identifier part
142
+ for (const p of prefixes) {
143
+ if (key.startsWith(p)) {
144
+ // For "GCP_CLIENT_EMAIL_ORG1", split gives ["GCP", "CLIENT", "EMAIL", "ORG1"]
145
+ // We want the part after the prefix, so we need to count underscores in prefix
146
+ const prefixParts = p.split("_").filter((x) => x);
147
+ const identifier = parts.slice(prefixParts.length).join("_");
148
+ return identifier.toLowerCase();
149
+ }
150
+ }
151
+ return "";
152
+ });
153
+
154
+ // Scan all environment variables
155
+ for (const key of Object.keys(process.env)) {
156
+ // Find matching prefix
157
+ for (const prefix of prefixes) {
158
+ if (key.startsWith(prefix)) {
159
+ const identifier = identifierExtractor(key, prefix);
160
+ if (!identifier) continue;
161
+
162
+ if (!result[identifier]) {
163
+ result[identifier] = {};
164
+ }
165
+
166
+ const propertyName = options.propertyMap[prefix];
167
+ if (propertyName) {
168
+ (result[identifier] as any)[propertyName] = process.env[key];
169
+ }
170
+ break; // Only match first prefix
171
+ }
172
+ }
173
+ }
174
+
175
+ // Validate that we found at least one group
176
+ if (Object.keys(result).length === 0 && options.required !== false) {
177
+ const errorMessage =
178
+ options.errorMessage ||
179
+ `No environment variables found matching prefixes: ${prefixes.join(
180
+ ", "
181
+ )}`;
182
+ throw new Error(errorMessage);
183
+ }
184
+
185
+ return result;
186
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Fragment - Content Abstraction Interface
3
+ *
4
+ * The Fragment interface represents the fundamental abstraction for all content
5
+ * types in the AI Executive system. It provides a unified way to handle
6
+ * different types of content (emails, documents, attachments, etc.) through
7
+ * a common interface.
8
+ *
9
+ * Fragments support multiple serving modes and can be processed, analyzed,
10
+ * and transformed while maintaining their original structure and metadata.
11
+ *
12
+ * @interface Fragment
13
+ * @version 1.0.0
14
+ * @author Divizend GmbH
15
+ */
16
+
17
+ import { FragmentServingMode } from "./FragmentServingMode";
18
+
19
+ export interface Fragment {
20
+ /**
21
+ * Unique identifier for this fragment
22
+ *
23
+ * The URI provides a standardized way to reference and locate
24
+ * the fragment within the system, regardless of its content type.
25
+ */
26
+ readonly uri: string;
27
+
28
+ /**
29
+ * Serves the fragment content in the specified format
30
+ *
31
+ * This method handles the transformation and delivery of fragment content
32
+ * based on the requested serving mode. It ensures proper content type
33
+ * headers and data formatting for different use cases.
34
+ *
35
+ * @param format - The desired serving mode (original, markdown, JSON, etc.)
36
+ * @returns Promise containing headers and data for the served content
37
+ * @throws Error if the serving mode is not supported or content processing fails
38
+ */
39
+ serve(format: FragmentServingMode): Promise<{
40
+ headers: { name: string; value: string }[];
41
+ data: Buffer;
42
+ }>;
43
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Fragment Serving Modes
3
+ *
4
+ * Defines the different ways in which fragment content can be served.
5
+ * Each mode represents a different format or representation of the
6
+ * original content, optimized for different use cases.
7
+ *
8
+ * @enum FragmentServingMode
9
+ * @version 1.0.0
10
+ * @author Divizend GmbH
11
+ */
12
+
13
+ export enum FragmentServingMode {
14
+ /**
15
+ * Original content format as stored in the system
16
+ *
17
+ * Serves the fragment in its native format with original
18
+ * encoding, headers, and structure preserved.
19
+ */
20
+ ORIGINAL = "original",
21
+
22
+ /**
23
+ * JSON metadata and content representation
24
+ *
25
+ * Provides structured access to fragment metadata, content,
26
+ * and relationships in a machine-readable format.
27
+ */
28
+ JSON = "json",
29
+
30
+ /**
31
+ * Markdown conversion of the original content
32
+ *
33
+ * Converts HTML or plain text content to Markdown format
34
+ * for easier reading and processing.
35
+ */
36
+ MARKDOWN = "markdown",
37
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * JSON Schema Validator - Schema Validation Utilities
3
+ *
4
+ * Provides JSON schema validation functionality for the Universe system.
5
+ * Uses a default implementation that can be extended or replaced.
6
+ *
7
+ * @module JsonSchemaValidator
8
+ * @version 1.0.0
9
+ */
10
+
11
+ export interface JsonSchema {
12
+ type: "object";
13
+ properties?: {
14
+ [key: string]: {
15
+ type: "string" | "number" | "boolean" | "array" | "object";
16
+ default?: any;
17
+ description?: string;
18
+ enum?: any[];
19
+ items?: JsonSchema | { type: string };
20
+ required?: boolean;
21
+ [key: string]: any; // Allow additional JSON schema properties
22
+ };
23
+ };
24
+ required?: string[];
25
+ additionalProperties?: boolean;
26
+ [key: string]: any; // Allow additional JSON schema properties
27
+ }
28
+
29
+ export interface ValidationResult {
30
+ valid: boolean;
31
+ errors: string[];
32
+ data?: any; // Validated and coerced data
33
+ }
34
+
35
+ /**
36
+ * Abstract JSON Schema Validator
37
+ * Provides a default implementation that can be extended
38
+ */
39
+ export class JsonSchemaValidator {
40
+ /**
41
+ * Validates data against a JSON schema
42
+ * Default implementation performs basic validation
43
+ *
44
+ * @param schema - JSON schema to validate against
45
+ * @param data - Data to validate
46
+ * @returns ValidationResult with validation status and errors
47
+ */
48
+ validate(schema: JsonSchema, data: any): ValidationResult {
49
+ const errors: string[] = [];
50
+ const validated: any = {};
51
+
52
+ if (!schema || schema.type !== "object") {
53
+ return {
54
+ valid: false,
55
+ errors: ["Schema must be an object type"],
56
+ };
57
+ }
58
+
59
+ // Check required properties
60
+ if (schema.required) {
61
+ for (const prop of schema.required) {
62
+ if (
63
+ !(prop in data) ||
64
+ data[prop] === undefined ||
65
+ data[prop] === null ||
66
+ data[prop] === ""
67
+ ) {
68
+ errors.push(`Missing required property: ${prop}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ // Validate and coerce properties
74
+ if (schema.properties) {
75
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
76
+ const value = data[key];
77
+
78
+ // Apply default if value is missing
79
+ if (
80
+ (value === undefined || value === null || value === "") &&
81
+ propSchema.default !== undefined
82
+ ) {
83
+ validated[key] = propSchema.default;
84
+ continue;
85
+ }
86
+
87
+ // Skip validation if property is not required and not provided
88
+ const isRequired = schema.required?.includes(key) ?? false;
89
+ if (
90
+ !isRequired &&
91
+ (value === undefined || value === null || value === "")
92
+ ) {
93
+ continue;
94
+ }
95
+
96
+ // Type validation and coercion
97
+ if (value !== undefined && value !== null && value !== "") {
98
+ const coerced = this.coerceValue(value, propSchema);
99
+ if (coerced === null) {
100
+ errors.push(
101
+ `Property ${key} has invalid type. Expected ${propSchema.type}`
102
+ );
103
+ } else {
104
+ validated[key] = coerced;
105
+ }
106
+ } else if (isRequired) {
107
+ errors.push(`Missing required property: ${key}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ return {
113
+ valid: errors.length === 0,
114
+ errors,
115
+ data: validated,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Coerces a value to match the schema type
121
+ *
122
+ * @private
123
+ * @param value - Value to coerce
124
+ * @param propSchema - Property schema definition
125
+ * @returns Coerced value or null if coercion fails
126
+ */
127
+ private coerceValue(value: any, propSchema: any): any {
128
+ const expectedType = propSchema.type;
129
+
130
+ switch (expectedType) {
131
+ case "string":
132
+ return String(value);
133
+ case "number":
134
+ const num = Number(value);
135
+ return isNaN(num) ? null : num;
136
+ case "boolean":
137
+ if (typeof value === "string") {
138
+ return value === "true" || value === "1";
139
+ }
140
+ return Boolean(value);
141
+ case "array":
142
+ if (Array.isArray(value)) {
143
+ return value;
144
+ }
145
+ // Try to parse as JSON array
146
+ try {
147
+ const parsed = JSON.parse(value);
148
+ if (Array.isArray(parsed)) {
149
+ return parsed;
150
+ }
151
+ } catch {
152
+ // Not valid JSON
153
+ }
154
+ return null;
155
+ case "object":
156
+ if (typeof value === "object" && value !== null) {
157
+ return value;
158
+ }
159
+ // Try to parse as JSON object
160
+ try {
161
+ const parsed = JSON.parse(value);
162
+ if (typeof parsed === "object" && parsed !== null) {
163
+ return parsed;
164
+ }
165
+ } catch {
166
+ // Not valid JSON
167
+ }
168
+ return null;
169
+ default:
170
+ return value;
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ProjectRoot - Utility to reliably determine the project root directory
3
+ *
4
+ * This function walks up the directory tree from the current file's location
5
+ * to find the project root by looking for marker files (package.json, tsconfig.json).
6
+ * The result is cached for performance.
7
+ */
8
+
9
+ import { resolve, join, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { stat } from "node:fs/promises";
12
+
13
+ let cachedProjectRoot: string | null = null;
14
+
15
+ /**
16
+ * Get the project root directory with 100% certainty
17
+ * Uses import.meta.url to get the current file location and walks up to find package.json
18
+ * @returns Absolute path to the project root
19
+ */
20
+ export async function getProjectRoot(): Promise<string> {
21
+ // Return cached value if available
22
+ if (cachedProjectRoot) {
23
+ return cachedProjectRoot;
24
+ }
25
+
26
+ // Get the directory of the current file (this file)
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+
30
+ // Start from the current file's directory and walk up
31
+ let currentDir = resolve(__dirname);
32
+
33
+ // Marker files that indicate project root
34
+ const markers = ["package.json", "tsconfig.json"];
35
+
36
+ // Walk up the directory tree
37
+ while (currentDir !== dirname(currentDir)) {
38
+ // Check if any marker file exists in current directory
39
+ for (const marker of markers) {
40
+ try {
41
+ const markerPath = join(currentDir, marker);
42
+ await stat(markerPath);
43
+ // Found a marker file - this is the project root
44
+ cachedProjectRoot = currentDir;
45
+ return cachedProjectRoot;
46
+ } catch {
47
+ // Marker file doesn't exist, continue
48
+ }
49
+ }
50
+
51
+ // Move up one directory
52
+ currentDir = dirname(currentDir);
53
+ }
54
+
55
+ // Fallback: if we reach the filesystem root without finding a marker,
56
+ // use the directory containing this utility file and walk up to src/../
57
+ // This is a last resort fallback
58
+ const fallbackRoot = resolve(__dirname, "..", "..");
59
+ cachedProjectRoot = fallbackRoot;
60
+ return cachedProjectRoot;
61
+ }
62
+
63
+ /**
64
+ * Synchronous version - uses cached value or throws if not yet cached
65
+ * Call getProjectRoot() first to ensure cache is populated
66
+ * @returns Absolute path to the project root
67
+ * @throws Error if project root hasn't been determined yet
68
+ */
69
+ export function getProjectRootSync(): string {
70
+ if (!cachedProjectRoot) {
71
+ throw new Error(
72
+ "Project root not yet determined. Call getProjectRoot() first."
73
+ );
74
+ }
75
+ return cachedProjectRoot;
76
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Scratch - Type definitions for Scratch endpoint system
3
+ */
4
+
5
+ import { Universe, UniverseModule } from "./Universe";
6
+
7
+ export interface ScratchBlock {
8
+ opcode: string;
9
+ blockType: "command" | "reporter" | "boolean" | "hat";
10
+ text: string;
11
+ schema?: {
12
+ [key: string]: {
13
+ type: "string" | "number" | "boolean" | "array" | "object" | "json";
14
+ default?: any;
15
+ description?: string;
16
+ schema?: {
17
+ // Property-level JSON schema (not full JsonSchema)
18
+ type: "string" | "number" | "boolean" | "array" | "object";
19
+ default?: any;
20
+ items?: any;
21
+ properties?: any;
22
+ [key: string]: any;
23
+ }; // Required when type is "json"
24
+ [key: string]: any; // Allow additional JSON schema properties
25
+ };
26
+ };
27
+ }
28
+
29
+ export interface ScratchContext {
30
+ userEmail?: string;
31
+ inputs?: any; // Validated request body/query params (set after validation)
32
+ universe?: Universe | null; // Universe instance (set by context middleware)
33
+ authHeader?: string; // Authorization header for nested endpoint calls
34
+ result?: any; // Result from nested endpoint calls
35
+ requestHost?: string; // Request host header (e.g., "localhost:3000" or "scratch.divizend.ai")
36
+ }
37
+
38
+ export interface ScratchEndpointDefinition {
39
+ block: (context: ScratchContext) => Promise<ScratchBlock>;
40
+ handler: (context: ScratchContext) => Promise<any>;
41
+ noAuth?: boolean;
42
+ /** Array of required Universe modules that must be initialized before handler execution */
43
+ requiredModules?: UniverseModule[];
44
+ }