@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/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
|
+
}
|
package/core/Fragment.ts
ADDED
|
@@ -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
|
+
}
|
package/core/Scratch.ts
ADDED
|
@@ -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
|
+
}
|