@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/basic/demo.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Single source of truth for the default BASIC demo code
3
+ */
4
+ export const DEFAULT_BASIC_DEMO = `# count from [start] to [end] in [stream]
5
+ # {"start":{"type":"number"},"end":{"type":"number"}}
6
+ x = $.inputs.start
7
+ while x <= $.inputs.end
8
+ call appendToStream $.inputs.stream {"count": x}
9
+ x = x + 1
10
+ end
11
+ return x`;
package/basic/index.ts ADDED
@@ -0,0 +1,490 @@
1
+ /**
2
+ * BASIC Compiler Module
3
+ * Compiles BASIC code to TypeScript endpoint definitions
4
+ */
5
+
6
+ interface SchemaDefinition {
7
+ [key: string]: {
8
+ type: "string" | "number" | "boolean" | "array" | "object" | "json";
9
+ default?: any;
10
+ description?: string;
11
+ schema?: any;
12
+ [key: string]: any;
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Parses BASIC values to TypeScript expressions
18
+ * Passes through expressions 1:1 without parsing
19
+ */
20
+ class ValueParser {
21
+ parse(value: string): string {
22
+ // Pass through the value as-is, character by character
23
+ return value.trim();
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Parses BASIC conditions to TypeScript conditions
29
+ * Passes through conditions 1:1 without parsing
30
+ */
31
+ class ConditionParser {
32
+ parse(condition: string): string {
33
+ // Pass through the condition as-is, character by character
34
+ return condition.trim();
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Parses call arguments from BASIC call statements
40
+ */
41
+ class CallArgsParser {
42
+ parse(callExpr: string): string[] {
43
+ const args: string[] = [];
44
+ let current = "";
45
+ let inString = false;
46
+ let stringChar = "";
47
+ let depth = 0;
48
+
49
+ for (let i = 0; i < callExpr.length; i++) {
50
+ const char = callExpr[i];
51
+
52
+ if (!inString) {
53
+ if (char === '"' || char === "'") {
54
+ inString = true;
55
+ stringChar = char;
56
+ current += char;
57
+ } else if (char === " " && depth === 0) {
58
+ if (current.trim()) {
59
+ args.push(current.trim());
60
+ current = "";
61
+ }
62
+ } else {
63
+ if (char === "{" || char === "[") depth++;
64
+ if (char === "}" || char === "]") depth--;
65
+ current += char;
66
+ }
67
+ } else {
68
+ current += char;
69
+ if (char === stringChar && callExpr[i - 1] !== "\\") {
70
+ inString = false;
71
+ }
72
+ }
73
+ }
74
+
75
+ if (current.trim()) {
76
+ args.push(current.trim());
77
+ }
78
+
79
+ return args;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Generates schema from text and optional schema definition
85
+ */
86
+ class SchemaGenerator {
87
+ /**
88
+ * Extracts parameter names from text (e.g., "do something [param1] with [param2]")
89
+ */
90
+ private extractParameterNames(text: string): string[] {
91
+ const matches = text.match(/\[([^\]]+)\]/g);
92
+ if (!matches) return [];
93
+ return matches.map((match) => match.slice(1, -1));
94
+ }
95
+
96
+ /**
97
+ * Generates default schema from parameter names
98
+ */
99
+ private generateDefaultSchema(parameterNames: string[]): SchemaDefinition {
100
+ const schema: SchemaDefinition = {};
101
+ for (const paramName of parameterNames) {
102
+ schema[paramName] = {
103
+ type: "string",
104
+ default: `[${paramName}]`,
105
+ };
106
+ }
107
+ return schema;
108
+ }
109
+
110
+ /**
111
+ * Parses schema from comment line (JSON format)
112
+ */
113
+ private parseSchemaComment(commentLine: string): SchemaDefinition | null {
114
+ try {
115
+ // Remove # and trim
116
+ const jsonStr = commentLine.replace(/^#\s*/, "").trim();
117
+ if (!jsonStr) return null;
118
+ return JSON.parse(jsonStr);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Merges default schema with optional schema
126
+ */
127
+ private mergeSchemas(
128
+ defaultSchema: SchemaDefinition,
129
+ optionalSchema: SchemaDefinition | null
130
+ ): SchemaDefinition {
131
+ if (!optionalSchema) return defaultSchema;
132
+
133
+ const merged = { ...defaultSchema };
134
+ for (const [key, value] of Object.entries(optionalSchema)) {
135
+ merged[key] = { ...defaultSchema[key], ...value };
136
+ }
137
+ return merged;
138
+ }
139
+
140
+ /**
141
+ * Generates schema from text and optional schema comment
142
+ */
143
+ generate(text: string, schemaComment?: string): SchemaDefinition {
144
+ const parameterNames = this.extractParameterNames(text);
145
+ const defaultSchema = this.generateDefaultSchema(parameterNames);
146
+ const optionalSchema = schemaComment
147
+ ? this.parseSchemaComment(schemaComment)
148
+ : null;
149
+ return this.mergeSchemas(defaultSchema, optionalSchema);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Parses BASIC statements and converts them to TypeScript
155
+ */
156
+ class BasicParser {
157
+ private valueParser: ValueParser;
158
+ private conditionParser: ConditionParser;
159
+ private callArgsParser: CallArgsParser;
160
+ private statements: string[] = [];
161
+ private indent: number = 4;
162
+ private hasReturn: boolean = false;
163
+ private variables: Set<string> = new Set();
164
+
165
+ constructor() {
166
+ this.valueParser = new ValueParser();
167
+ this.conditionParser = new ConditionParser();
168
+ this.callArgsParser = new CallArgsParser();
169
+ }
170
+
171
+ parse(lines: string[]): string {
172
+ this.statements = [];
173
+ this.indent = 4;
174
+ this.hasReturn = false;
175
+ this.variables.clear();
176
+
177
+ for (const line of lines) {
178
+ this.parseLine(line.trim());
179
+ }
180
+
181
+ // Add default return if none provided
182
+ if (!this.hasReturn) {
183
+ this.statements.push(
184
+ `${" ".repeat(this.indent)}return { success: true };`
185
+ );
186
+ }
187
+
188
+ const handlerBody =
189
+ this.statements.length > 0
190
+ ? this.statements.join("\n")
191
+ : " return { success: true };";
192
+
193
+ // Replace $ with context (but not inside strings)
194
+ return this.replaceDollarSignWithContext(handlerBody);
195
+ }
196
+
197
+ /**
198
+ * Replaces $ with context in the generated code, but preserves $ inside strings
199
+ */
200
+ private replaceDollarSignWithContext(code: string): string {
201
+ let result = "";
202
+ let inString = false;
203
+ let stringChar = "";
204
+ let i = 0;
205
+
206
+ while (i < code.length) {
207
+ const char = code[i];
208
+ const prevChar = i > 0 ? code[i - 1] : "";
209
+
210
+ if (!inString) {
211
+ if (char === '"' || char === "'") {
212
+ inString = true;
213
+ stringChar = char;
214
+ result += char;
215
+ i++;
216
+ } else if (char === "$") {
217
+ // Check what follows $
218
+ const nextChar = i + 1 < code.length ? code[i + 1] : "";
219
+
220
+ if (nextChar === ".") {
221
+ // $.property -> context.property
222
+ result += "context";
223
+ i++;
224
+ } else if (/[a-zA-Z_$]/.test(nextChar)) {
225
+ // $property -> context.property
226
+ result += "context.";
227
+ i++;
228
+ } else {
229
+ // $ alone or $ followed by non-identifier -> context
230
+ result += "context";
231
+ i++;
232
+ }
233
+ } else {
234
+ result += char;
235
+ i++;
236
+ }
237
+ } else {
238
+ result += char;
239
+ if (char === stringChar && prevChar !== "\\") {
240
+ inString = false;
241
+ }
242
+ i++;
243
+ }
244
+ }
245
+
246
+ return result;
247
+ }
248
+
249
+ private parseLine(trimmed: string): void {
250
+ // Variable assignment: x = value
251
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*/.test(trimmed)) {
252
+ this.parseAssignment(trimmed);
253
+ }
254
+ // While loop: while condition
255
+ else if (trimmed.startsWith("while ")) {
256
+ this.parseWhile(trimmed);
257
+ }
258
+ // End (closes while/if)
259
+ else if (trimmed === "end") {
260
+ this.parseEnd();
261
+ }
262
+ // If statement: if condition
263
+ else if (trimmed.startsWith("if ")) {
264
+ this.parseIf(trimmed);
265
+ }
266
+ // Else
267
+ else if (trimmed === "else") {
268
+ this.parseElse();
269
+ }
270
+ // Return statement: return value
271
+ else if (trimmed.startsWith("return ")) {
272
+ this.parseReturn(trimmed);
273
+ }
274
+ // Endpoint call: call endpointName arg1 arg2 ...
275
+ else if (trimmed.startsWith("call ")) {
276
+ this.parseCall(trimmed);
277
+ }
278
+ // Unknown
279
+ else {
280
+ throw new Error(`Unknown statement: ${trimmed}`);
281
+ }
282
+ }
283
+
284
+ private parseAssignment(trimmed: string): void {
285
+ const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
286
+ if (match) {
287
+ const varName = match[1];
288
+ const value = match[2];
289
+ this.variables.add(varName);
290
+ const tsValue = this.valueParser.parse(value);
291
+ const isFirstUse = !this.statements.some((s) =>
292
+ s.includes(`${varName} =`)
293
+ );
294
+ const decl = isFirstUse ? "let " : "";
295
+ this.statements.push(
296
+ `${" ".repeat(this.indent)}${decl}${varName} = ${tsValue};`
297
+ );
298
+ }
299
+ }
300
+
301
+ private parseWhile(trimmed: string): void {
302
+ const condition = trimmed.substring(6).trim();
303
+ const tsCondition = this.conditionParser.parse(condition);
304
+ this.statements.push(`${" ".repeat(this.indent)}while (${tsCondition}) {`);
305
+ this.indent += 2;
306
+ }
307
+
308
+ private parseEnd(): void {
309
+ this.indent = Math.max(4, this.indent - 2);
310
+ this.statements.push(`${" ".repeat(this.indent)}}`);
311
+ }
312
+
313
+ private parseIf(trimmed: string): void {
314
+ const condition = trimmed.substring(3).trim();
315
+ const tsCondition = this.conditionParser.parse(condition);
316
+ this.statements.push(`${" ".repeat(this.indent)}if (${tsCondition}) {`);
317
+ this.indent += 2;
318
+ }
319
+
320
+ private parseElse(): void {
321
+ this.indent = Math.max(4, this.indent - 2);
322
+ this.statements.push(`${" ".repeat(this.indent)}} else {`);
323
+ this.indent += 2;
324
+ }
325
+
326
+ private parseReturn(trimmed: string): void {
327
+ const value = trimmed.substring(7).trim();
328
+ const tsValue = this.valueParser.parse(value);
329
+ this.statements.push(`${" ".repeat(this.indent)}return ${tsValue};`);
330
+ this.hasReturn = true;
331
+ }
332
+
333
+ private parseCall(trimmed: string): void {
334
+ const callExpr = trimmed.substring(5).trim();
335
+ const parts = this.callArgsParser.parse(callExpr);
336
+ if (parts.length === 0) {
337
+ throw new Error(`Invalid call statement: ${trimmed}`);
338
+ }
339
+ const endpointName = parts[0];
340
+ const args = parts.slice(1);
341
+
342
+ // Parse arguments
343
+ const parsedArgs = args.map((arg) => this.valueParser.parse(arg));
344
+ const argsStr = parsedArgs.join(", ");
345
+
346
+ // Use Universe.call() method for clean endpoint calling
347
+ this.statements.push(
348
+ `${" ".repeat(
349
+ this.indent
350
+ )}context.result = await context.universe!.call(context, "${endpointName}"${argsStr ? `, ${argsStr}` : ""});`
351
+ );
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Generates TypeScript code from parsed BASIC
357
+ */
358
+ class TypeScriptGenerator {
359
+ private schemaGenerator: SchemaGenerator;
360
+
361
+ constructor() {
362
+ this.schemaGenerator = new SchemaGenerator();
363
+ }
364
+
365
+ generate(
366
+ opcode: string,
367
+ text: string,
368
+ schemaComment: string | undefined,
369
+ handlerBody: string,
370
+ basicCode: string
371
+ ): string {
372
+ const schema = this.schemaGenerator.generate(text, schemaComment);
373
+
374
+ // Format schema as TypeScript object with proper indentation
375
+ const schemaLines = JSON.stringify(schema, null, 2)
376
+ .split("\n")
377
+ .map((line, idx) => {
378
+ if (idx === 0) return " " + line;
379
+ return " " + line;
380
+ })
381
+ .join("\n");
382
+
383
+ // Format BASIC code as a comment block
384
+ const basicCodeComment = basicCode
385
+ .split("\n")
386
+ .map((line) => ` * ${line}`)
387
+ .join("\n");
388
+
389
+ return `/**
390
+ ${basicCodeComment}
391
+ */
392
+ import type { ScratchEndpointDefinition } from "../index";
393
+
394
+ export const ${opcode}: ScratchEndpointDefinition = {
395
+ block: async () => ({
396
+ opcode: "${opcode}",
397
+ blockType: "command",
398
+ text: ${JSON.stringify(text)},
399
+ schema: ${schemaLines},
400
+ }),
401
+ handler: async (context) => {
402
+ ${handlerBody}
403
+ },
404
+ };
405
+ `;
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Main BASIC Compiler class
411
+ */
412
+ export class BasicCompiler {
413
+ private parser: BasicParser;
414
+ private generator: TypeScriptGenerator;
415
+
416
+ constructor() {
417
+ this.parser = new BasicParser();
418
+ this.generator = new TypeScriptGenerator();
419
+ }
420
+
421
+ /**
422
+ * Compiles BASIC code to TypeScript endpoint definition
423
+ * @param basicCode - BASIC source code (first line is text, optional second line is schema comment)
424
+ * @param opcode - The opcode for the generated endpoint
425
+ * @returns TypeScript source code for the endpoint
426
+ */
427
+ compile(basicCode: string, opcode: string): string {
428
+ const lines = basicCode.split("\n").map((line) => line.trim());
429
+
430
+ // Extract text from first line (mandatory)
431
+ if (lines.length === 0) {
432
+ throw new Error("BASIC code must have at least a text line");
433
+ }
434
+
435
+ let textLine = lines[0];
436
+ let schemaComment: string | undefined;
437
+ let codeStartIndex = 1;
438
+
439
+ // First line must be the text (can be a comment starting with #)
440
+ if (textLine.startsWith("#")) {
441
+ textLine = textLine.substring(1).trim();
442
+ }
443
+
444
+ if (!textLine) {
445
+ throw new Error("First line must contain the endpoint text");
446
+ }
447
+
448
+ // Second line (optional) can be a schema comment
449
+ if (lines.length > 1 && lines[1].startsWith("#")) {
450
+ schemaComment = lines[1];
451
+ codeStartIndex = 2;
452
+ }
453
+
454
+ // Parse the actual BASIC code (skip comments, but keep non-comment lines)
455
+ const codeLines = lines
456
+ .slice(codeStartIndex)
457
+ .map((line) => {
458
+ // Remove comments from code lines
459
+ const commentIndex = line.indexOf("#");
460
+ if (commentIndex >= 0) {
461
+ return line.substring(0, commentIndex).trim();
462
+ }
463
+ return line.trim();
464
+ })
465
+ .filter((line) => line.length > 0);
466
+
467
+ // Parse BASIC code to TypeScript
468
+ const handlerBody = this.parser.parse(codeLines);
469
+
470
+ // Generate TypeScript
471
+ return this.generator.generate(
472
+ opcode,
473
+ textLine,
474
+ schemaComment,
475
+ handlerBody,
476
+ basicCode
477
+ );
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Convenience function for backward compatibility
483
+ */
484
+ export function compileBasicToTypeScript(
485
+ basicCode: string,
486
+ opcode: string
487
+ ): string {
488
+ const compiler = new BasicCompiler();
489
+ return compiler.compile(basicCode, opcode);
490
+ }
package/core/Auth.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Auth - Authentication module for Universe
3
+ *
4
+ * Handles JWT token validation and signing
5
+ */
6
+
7
+ import { jwtVerify, SignJWT } from "jose";
8
+ import { env } from "./Env";
9
+
10
+ export class Auth {
11
+ private jwtSecretKey: Uint8Array | null;
12
+
13
+ constructor() {
14
+ const jwtSecret = env("WEB_UI_JWT_SECRET", {
15
+ required: false,
16
+ defaultValue: "",
17
+ });
18
+ this.jwtSecretKey = jwtSecret ? new TextEncoder().encode(jwtSecret) : null;
19
+ }
20
+
21
+ /**
22
+ * Validate JWT token and extract payload
23
+ */
24
+ async validateJwtToken(token: string): Promise<any | null> {
25
+ if (!this.jwtSecretKey) {
26
+ return null;
27
+ }
28
+
29
+ try {
30
+ const { payload } = await jwtVerify(token, this.jwtSecretKey);
31
+ return payload;
32
+ } catch (error) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Sign/create a JWT token
39
+ */
40
+ async signJwtToken(
41
+ payload: { email: string },
42
+ expirationTime: string = "30d"
43
+ ): Promise<string> {
44
+ if (!this.jwtSecretKey) {
45
+ throw new Error("JWT secret not configured");
46
+ }
47
+
48
+ const jwt = await new SignJWT(payload)
49
+ .setProtectedHeader({ alg: "HS256" })
50
+ .setIssuedAt()
51
+ .setExpirationTime(expirationTime)
52
+ .sign(this.jwtSecretKey);
53
+
54
+ return jwt;
55
+ }
56
+
57
+ /**
58
+ * Check if JWT authentication is configured
59
+ */
60
+ isConfigured(): boolean {
61
+ return this.jwtSecretKey !== null;
62
+ }
63
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Currency Utility Functions
3
+ *
4
+ * Provides utility functions for currency handling, conversion, and formatting
5
+ * in the AI Executive system. This module handles financial data processing
6
+ * for business workflows and invoice generation.
7
+ *
8
+ * @module Currency
9
+ * @version 1.0.0
10
+ * @author Divizend GmbH
11
+ */
12
+
13
+ export class Currency {
14
+ // Currency utility methods will be implemented here
15
+ // as the system expands to handle financial operations
16
+ }