@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
@@ -0,0 +1,3 @@
1
+ export * from "./HttpServer";
2
+ export * from "./NativeHttpServer";
3
+
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CORS Middleware
3
+ * Handles CORS preflight requests and sets CORS headers
4
+ */
5
+
6
+ import { Middleware, MiddlewareContext } from "./types";
7
+
8
+ export const corsMiddleware: Middleware = async (ctx, next) => {
9
+ const { req, res } = ctx;
10
+
11
+ // Handle CORS preflight
12
+ if (req.method === "OPTIONS") {
13
+ setCorsHeaders(res);
14
+ res.writeHead(200);
15
+ res.end();
16
+ return;
17
+ }
18
+
19
+ // Set CORS headers for all responses
20
+ setCorsHeaders(res);
21
+ await next();
22
+ };
23
+
24
+ function setCorsHeaders(res: any): void {
25
+ res.setHeader("Access-Control-Allow-Origin", "*");
26
+ res.setHeader(
27
+ "Access-Control-Allow-Methods",
28
+ "GET, POST, PUT, DELETE, OPTIONS"
29
+ );
30
+ res.setHeader("Access-Control-Allow-Headers", "*");
31
+ res.setHeader("Access-Control-Expose-Headers", "*");
32
+ }
33
+
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Static File Middleware
3
+ * Serves static files from the configured root directory
4
+ */
5
+
6
+ import { readFile, stat } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { Middleware, MiddlewareContext } from "./types";
9
+
10
+ export interface StaticMiddlewareOptions {
11
+ rootPath: string | null;
12
+ }
13
+
14
+ export function createStaticMiddleware(
15
+ options: StaticMiddlewareOptions
16
+ ): Middleware {
17
+ return async (ctx, next) => {
18
+ const { req, res, context } = ctx;
19
+ const path = context.path || "";
20
+
21
+ if (!options.rootPath || !path.startsWith("/public/")) {
22
+ await next();
23
+ return;
24
+ }
25
+
26
+ try {
27
+ const filePath = join(options.rootPath, path.substring(8)); // Remove "/public/"
28
+ const stats = await stat(filePath);
29
+ if (!stats.isFile()) {
30
+ await next();
31
+ return;
32
+ }
33
+
34
+ const content = await readFile(filePath);
35
+ const ext = filePath.split(".").pop()?.toLowerCase();
36
+ const contentTypeMap: Record<string, string> = {
37
+ html: "text/html",
38
+ css: "text/css",
39
+ js: "application/javascript",
40
+ json: "application/json",
41
+ png: "image/png",
42
+ jpg: "image/jpeg",
43
+ jpeg: "image/jpeg",
44
+ gif: "image/gif",
45
+ svg: "image/svg+xml",
46
+ };
47
+ const contentType = contentTypeMap[ext || ""] || "application/octet-stream";
48
+
49
+ setNoCacheHeaders(res);
50
+ res.writeHead(200, { "Content-Type": contentType });
51
+ res.end(content);
52
+ // Don't call next() - we've handled the request
53
+ } catch (error) {
54
+ await next();
55
+ }
56
+ };
57
+ }
58
+
59
+ function setNoCacheHeaders(res: any): void {
60
+ res.setHeader(
61
+ "Cache-Control",
62
+ "no-store, no-cache, must-revalidate, max-age=0"
63
+ );
64
+ res.setHeader("Pragma", "no-cache");
65
+ res.setHeader("Expires", "0");
66
+ }
67
+
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Request Logger Middleware
3
+ * Logs incoming requests and responses with structured logging
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import { Middleware, MiddlewareContext } from "./types";
8
+
9
+ // Keep per-request metadata without leaking memory
10
+ const meta = new WeakMap<any, { id: string; start: number }>();
11
+
12
+ // Minimal structured logger - automatically adds timestamp unless explicitly provided
13
+ const log = (record: Record<string, unknown>) => {
14
+ if (!record.ts) {
15
+ record.ts = new Date().toISOString();
16
+ }
17
+ console.log(JSON.stringify(record));
18
+ };
19
+
20
+ // Best-effort client IP from common proxy/CDN headers
21
+ const getClientIP = (req: any): string | undefined => {
22
+ const headers = req.headers || {};
23
+ const getHeader = (name: string) => {
24
+ const value = headers[name] || headers[name.toLowerCase()];
25
+ return Array.isArray(value) ? value[0] : value;
26
+ };
27
+
28
+ const xForwardedFor = getHeader("x-forwarded-for");
29
+ if (xForwardedFor) {
30
+ return xForwardedFor.split(",")[0]?.trim();
31
+ }
32
+
33
+ return (
34
+ getHeader("x-real-ip") ||
35
+ getHeader("x-client-ip") ||
36
+ getHeader("cf-connecting-ip") ||
37
+ getHeader("fastly-client-ip") ||
38
+ getHeader("x-cluster-client-ip") ||
39
+ getHeader("x-forwarded") ||
40
+ getHeader("forwarded-for") ||
41
+ getHeader("forwarded") ||
42
+ getHeader("appengine-user-ip") ||
43
+ getHeader("true-client-ip") ||
44
+ getHeader("cf-pseudo-ipv4") ||
45
+ undefined
46
+ );
47
+ };
48
+
49
+ export const requestLoggerMiddleware: Middleware = async (ctx, next) => {
50
+ const { req, res, context, metadata } = ctx;
51
+
52
+ // Accept incoming request ID or generate one
53
+ const reqId =
54
+ req.headers?.["x-request-id"] ||
55
+ req.headers?.["X-Request-Id"] ||
56
+ "" ||
57
+ randomUUID();
58
+
59
+ // Set response header
60
+ res.setHeader("x-request-id", reqId);
61
+
62
+ // Stash timing data for this request
63
+ const start = performance.now();
64
+ metadata.requestId = reqId;
65
+ metadata.startTime = start;
66
+ meta.set(req, { id: reqId, start });
67
+
68
+ // Parse URL for logging - use query from context if available (already filtered)
69
+ const url = req.url || "/";
70
+ const urlObj = new URL(url, `http://${req.headers?.host || "localhost"}`);
71
+ const query: Record<string, string> =
72
+ context.query || Object.fromEntries(urlObj.searchParams);
73
+
74
+ // Base request log
75
+ log({
76
+ level: "info",
77
+ event: "request",
78
+ req_id: reqId,
79
+ method: req.method || "GET",
80
+ path: urlObj.pathname,
81
+ query: Object.keys(query).length > 0 ? query : undefined,
82
+ ip: getClientIP(req),
83
+ ua: req.headers?.["user-agent"] || undefined,
84
+ referer: req.headers?.referer || req.headers?.referrer || undefined,
85
+ req_len: Number(req.headers?.["content-length"] || "") || undefined,
86
+ content_type: req.headers?.["content-type"] || undefined,
87
+ });
88
+
89
+ log({
90
+ level: "info",
91
+ event: "after_request_log",
92
+ req_id: reqId,
93
+ path: urlObj.pathname,
94
+ query_keys: Object.keys(query),
95
+ });
96
+
97
+ // Track response status and length
98
+ let statusCode = 200;
99
+ let responseLength: number | undefined = undefined;
100
+
101
+ // Intercept writeHead to capture status
102
+ const originalWriteHead = res.writeHead.bind(res);
103
+ res.writeHead = function (status: number, headers?: any) {
104
+ statusCode = status;
105
+ if (headers && headers["content-length"]) {
106
+ responseLength = Number(headers["content-length"]) || undefined;
107
+ }
108
+ return originalWriteHead(status, headers);
109
+ };
110
+
111
+ // Intercept setHeader to capture content-length
112
+ const originalSetHeader = res.setHeader.bind(res);
113
+ res.setHeader = function (name: string, value: string | number) {
114
+ if (name.toLowerCase() === "content-length") {
115
+ responseLength = Number(value) || undefined;
116
+ }
117
+ return originalSetHeader(name, value);
118
+ };
119
+
120
+ try {
121
+ await next();
122
+
123
+ // Log response after next() completes
124
+ const m = meta.get(req);
125
+ const duration = m ? Math.round(performance.now() - m.start) : undefined;
126
+
127
+ log({
128
+ level: "info",
129
+ event: "response",
130
+ req_id: reqId,
131
+ status: statusCode,
132
+ duration_ms: duration,
133
+ res_len: responseLength,
134
+ });
135
+
136
+ meta.delete(req);
137
+ } catch (error) {
138
+ // Log error
139
+ const m = meta.get(req);
140
+ const reqIdForError = m?.id || reqId;
141
+ const urlObjForError = new URL(
142
+ req.url || "/",
143
+ `http://${req.headers?.host || "localhost"}`
144
+ );
145
+
146
+ log({
147
+ level: "error",
148
+ event: "error",
149
+ req_id: reqIdForError,
150
+ method: req.method || "GET",
151
+ path: urlObjForError.pathname,
152
+ status: statusCode,
153
+ message: error instanceof Error ? error.message : String(error),
154
+ });
155
+
156
+ meta.delete(req);
157
+ throw error;
158
+ }
159
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Body Parser Middleware
3
+ * Parses request body for POST/PUT/PATCH requests
4
+ */
5
+
6
+ import { Middleware, MiddlewareContext } from "./types";
7
+
8
+ async function readBody(req: any): Promise<any> {
9
+ return new Promise((resolve, reject) => {
10
+ // If body is already parsed (from fetch handler), use it
11
+ if ((req as any).body !== undefined) {
12
+ resolve((req as any).body);
13
+ return;
14
+ }
15
+
16
+ let body = "";
17
+ req.on("data", (chunk: any) => {
18
+ body += chunk.toString();
19
+ });
20
+ req.on("end", () => {
21
+ try {
22
+ const contentType = req.headers?.["content-type"] || "";
23
+ if (contentType.includes("application/json")) {
24
+ resolve(body ? JSON.parse(body) : {});
25
+ } else {
26
+ resolve(body || {});
27
+ }
28
+ } catch (error) {
29
+ reject(error);
30
+ }
31
+ });
32
+ req.on("error", reject);
33
+ });
34
+ }
35
+
36
+ export const bodyParserMiddleware: Middleware = async (ctx, next) => {
37
+ const { req, context } = ctx;
38
+ const method = req.method || "GET";
39
+
40
+ // Read body once if needed (for POST/PUT/PATCH)
41
+ if (["POST", "PUT", "PATCH"].includes(method)) {
42
+ try {
43
+ const requestBody = await readBody(req);
44
+ // Store body in request object for later use
45
+ (req as any).body = requestBody;
46
+ context.requestBody = requestBody;
47
+ } catch {
48
+ // Ignore body reading errors
49
+ }
50
+ }
51
+
52
+ await next();
53
+ };
54
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * No-Cache Middleware
3
+ * Sets no-cache headers for all responses
4
+ */
5
+
6
+ import { Middleware, MiddlewareContext } from "./types";
7
+
8
+ export const noCacheMiddleware: Middleware = async (ctx, next) => {
9
+ const { res } = ctx;
10
+
11
+ setNoCacheHeaders(res);
12
+ await next();
13
+ };
14
+
15
+ function setNoCacheHeaders(res: any): void {
16
+ res.setHeader(
17
+ "Cache-Control",
18
+ "no-store, no-cache, must-revalidate, max-age=0"
19
+ );
20
+ res.setHeader("Pragma", "no-cache");
21
+ res.setHeader("Expires", "0");
22
+ }
23
+
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Response Handler Middleware
3
+ * Handles converting handler responses to HTTP responses
4
+ */
5
+
6
+ import { Middleware, MiddlewareContext } from "./types";
7
+
8
+ export const responseHandlerMiddleware: Middleware = async (ctx, next) => {
9
+ const { req, res, context } = ctx;
10
+
11
+ // This middleware runs after routing, so if we get here,
12
+ // the route handler should have already set the response
13
+ // But we can add response formatting here if needed
14
+ await next();
15
+ };
16
+
17
+ /**
18
+ * Converts a handler result to an HTTP response
19
+ */
20
+ export function handleHandlerResult(result: any, res: any): void {
21
+ if (result instanceof Response) {
22
+ // Copy Response to Node response
23
+ res.writeHead(result.status, Object.fromEntries(result.headers));
24
+ result.text().then((body) => res.end(body));
25
+ return;
26
+ }
27
+ if (typeof result === "string") {
28
+ res.writeHead(200, { "Content-Type": "text/plain" });
29
+ res.end(result);
30
+ return;
31
+ }
32
+ if (result === null || result === undefined) {
33
+ res.writeHead(200, { "Content-Type": "application/json" });
34
+ res.end(JSON.stringify({ success: true }));
35
+ return;
36
+ }
37
+ res.writeHead(200, { "Content-Type": "application/json" });
38
+ res.end(JSON.stringify(result));
39
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Handler Wrapper Utilities
3
+ * Wraps handlers with auth and validation logic
4
+ */
5
+
6
+ import {
7
+ Universe,
8
+ ScratchContext,
9
+ ScratchBlock,
10
+ ScratchEndpointDefinition,
11
+ JsonSchema,
12
+ JsonSchemaValidator,
13
+ UniverseModule,
14
+ } from "../../core/index";
15
+ import { IncomingMessage, ServerResponse } from "node:http";
16
+
17
+ // Type alias for schema property values
18
+ type SchemaProperty = NonNullable<ScratchBlock["schema"]>[string];
19
+
20
+ export interface HandlerWrapperOptions {
21
+ universe: Universe;
22
+ endpoint: ScratchEndpointDefinition;
23
+ noAuth?: boolean;
24
+ requiredModules?: UniverseModule[];
25
+ }
26
+
27
+ /**
28
+ * Wraps a handler with authentication and validation
29
+ */
30
+ export async function wrapHandlerWithAuthAndValidation(
31
+ options: HandlerWrapperOptions
32
+ ): Promise<
33
+ (
34
+ context: ScratchContext,
35
+ query?: Record<string, string>,
36
+ requestBody?: any,
37
+ authHeader?: string
38
+ ) => Promise<any>
39
+ > {
40
+ const { universe, endpoint, noAuth = false, requiredModules = [] } = options;
41
+
42
+ // Get the block definition to extract schema
43
+ const blockDef = await endpoint.block({});
44
+ const schema = blockDef.schema;
45
+
46
+ // Create the wrapped handler
47
+ return async (
48
+ context: ScratchContext,
49
+ query: Record<string, string> = {},
50
+ requestBody: any = undefined,
51
+ authHeader: string | undefined = undefined
52
+ ) => {
53
+ // Auth check
54
+ if (!noAuth) {
55
+ if (!universe.auth.isConfigured()) {
56
+ throw new Error("JWT authentication not configured");
57
+ }
58
+
59
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
60
+ throw new Error("Missing or invalid authorization header");
61
+ }
62
+
63
+ const token = authHeader.substring(7);
64
+ try {
65
+ const payload = await universe.auth.validateJwtToken(token);
66
+ if (!payload) {
67
+ throw new Error("Invalid or expired token");
68
+ }
69
+ } catch {
70
+ throw new Error("Invalid or expired token");
71
+ }
72
+ }
73
+
74
+ // Extract user email from auth header if present
75
+ let userEmail: string | undefined;
76
+ try {
77
+ if (authHeader?.startsWith("Bearer ")) {
78
+ const payload = await universe.auth.validateJwtToken(
79
+ authHeader.substring(7)
80
+ );
81
+ if (payload) userEmail = (payload as any)?.email;
82
+ }
83
+ } catch {}
84
+
85
+ // Update context with user email, authHeader, and ensure universe is set
86
+ const enrichedContext: ScratchContext = {
87
+ ...context,
88
+ userEmail,
89
+ universe: universe,
90
+ authHeader: authHeader, // Store authHeader for nested calls
91
+ };
92
+
93
+ // Module validation
94
+ if (requiredModules.length > 0) {
95
+ const missingModules = requiredModules.filter(
96
+ (module) => !universe.hasModule(module)
97
+ );
98
+ if (missingModules.length > 0) {
99
+ throw new Error(
100
+ `Required modules not available: ${missingModules.join(", ")}`
101
+ );
102
+ }
103
+ }
104
+
105
+ // Schema validation
106
+ if (schema) {
107
+ const validator =
108
+ universe?.jsonSchemaValidator || new JsonSchemaValidator();
109
+ const fullSchema = constructJsonSchema(schema);
110
+ const isGet = blockDef.blockType === "reporter";
111
+
112
+ // For GET requests, query params go directly into inputs
113
+ // For POST requests, request body goes into inputs
114
+ let data: any = isGet
115
+ ? Object.fromEntries(
116
+ Object.keys(schema).map((key) => [key, query[key] || undefined])
117
+ )
118
+ : requestBody || {};
119
+
120
+ // Handle JSON type properties
121
+ if (schema) {
122
+ for (const [key, propSchema] of Object.entries(schema)) {
123
+ const typedPropSchema = propSchema as SchemaProperty;
124
+ if (
125
+ typedPropSchema.type === "json" &&
126
+ data[key] !== undefined &&
127
+ data[key] !== null &&
128
+ data[key] !== ""
129
+ ) {
130
+ try {
131
+ // If data[key] is already an object, use it directly
132
+ let parsed = data[key];
133
+ if (typeof data[key] === "string") {
134
+ parsed = JSON.parse(data[key]);
135
+ }
136
+ if (typedPropSchema.schema) {
137
+ const wrappedSchema: JsonSchema = {
138
+ type: "object",
139
+ properties: { value: typedPropSchema.schema },
140
+ required: ["value"],
141
+ };
142
+ const result = validator.validate(wrappedSchema, {
143
+ value: parsed,
144
+ });
145
+ if (!result.valid) {
146
+ throw new Error(
147
+ `Validation failed for ${key}: ${JSON.stringify(
148
+ result.errors
149
+ )}`
150
+ );
151
+ }
152
+ data[key] = result.data?.value ?? parsed;
153
+ } else {
154
+ data[key] = parsed;
155
+ }
156
+ } catch (parseError) {
157
+ throw new Error(
158
+ `Invalid JSON for ${key}: ${
159
+ parseError instanceof Error
160
+ ? parseError.message
161
+ : "Unknown error"
162
+ }`
163
+ );
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ // Validate the data
170
+ const dataForValidation: any = { ...data };
171
+ if (schema) {
172
+ for (const [key, propSchema] of Object.entries(schema)) {
173
+ const typedPropSchema = propSchema as SchemaProperty;
174
+ if (
175
+ typedPropSchema.type === "json" &&
176
+ dataForValidation[key] !== undefined
177
+ )
178
+ delete dataForValidation[key];
179
+ }
180
+ }
181
+
182
+ const result = validator.validate(fullSchema, dataForValidation);
183
+ if (!result.valid) {
184
+ throw new Error(`Validation failed: ${JSON.stringify(result.errors)}`);
185
+ }
186
+
187
+ const finalData = { ...result.data };
188
+ if (schema) {
189
+ for (const [key, propSchema] of Object.entries(schema)) {
190
+ const typedPropSchema = propSchema as SchemaProperty;
191
+ if (typedPropSchema.type === "json" && data[key] !== undefined)
192
+ finalData[key] = data[key];
193
+ }
194
+ }
195
+
196
+ enrichedContext.inputs = finalData;
197
+ } else {
198
+ // For endpoints without schema, set empty inputs
199
+ enrichedContext.inputs = {};
200
+ }
201
+
202
+ // Call the original handler
203
+ return await endpoint.handler(enrichedContext);
204
+ };
205
+ }
206
+
207
+ function constructJsonSchema(schema?: ScratchBlock["schema"]): JsonSchema {
208
+ if (!schema)
209
+ return {
210
+ type: "object",
211
+ properties: {},
212
+ required: [],
213
+ additionalProperties: false,
214
+ };
215
+ const properties: any = {};
216
+ const required: string[] = [];
217
+ for (const [key, propSchema] of Object.entries(schema)) {
218
+ // Type assertion: schema is defined as Record<string, {...}>, so propSchema is the value type
219
+ const typedPropSchema = propSchema as SchemaProperty;
220
+ if (typedPropSchema.type === "json") {
221
+ if (!typedPropSchema.schema)
222
+ throw new Error(
223
+ `Property ${key} has type "json" but no schema provided`
224
+ );
225
+ properties[key] = {
226
+ type: "string",
227
+ description: typedPropSchema.description,
228
+ _jsonSchema: typedPropSchema.schema,
229
+ };
230
+ } else {
231
+ // Copy schema but exclude non-JSON-Schema fields
232
+ const {
233
+ default: _,
234
+ description: __,
235
+ ...jsonSchemaProps
236
+ } = typedPropSchema;
237
+ properties[key] = jsonSchemaProps;
238
+ // Only add to required if there's no default or default is a placeholder
239
+ if (!typedPropSchema.default || typedPropSchema.default === `[${key}]`) {
240
+ required.push(key);
241
+ }
242
+ }
243
+ }
244
+ return {
245
+ type: "object",
246
+ properties,
247
+ required,
248
+ additionalProperties: false,
249
+ };
250
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Middleware exports
3
+ */
4
+
5
+ export * from "./types";
6
+ export * from "./01-cors";
7
+ export * from "./02-static";
8
+ export * from "./03-request-logger";
9
+ export * from "./04-body-parser";
10
+ export * from "./05-no-cache";
11
+ export * from "./06-response-handler";
12
+ export * from "./handler-wrapper";
13
+
14
+ import { Middleware } from "./types";
15
+ import { corsMiddleware } from "./01-cors";
16
+ import { createStaticMiddleware } from "./02-static";
17
+ import { requestLoggerMiddleware } from "./03-request-logger";
18
+ import { bodyParserMiddleware } from "./04-body-parser";
19
+ import { noCacheMiddleware } from "./05-no-cache";
20
+
21
+ /**
22
+ * Creates the middleware chain for NativeHttpServer
23
+ * @param staticRootPath - Root path for static files (or null)
24
+ * @returns Array of middlewares in execution order
25
+ */
26
+ export function createMiddlewareChain(
27
+ staticRootPath: string | null
28
+ ): Middleware[] {
29
+ return [
30
+ corsMiddleware,
31
+ createStaticMiddleware({ rootPath: staticRootPath }),
32
+ requestLoggerMiddleware,
33
+ bodyParserMiddleware,
34
+ noCacheMiddleware,
35
+ ];
36
+ }
37
+
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Middleware types for NativeHttpServer
3
+ */
4
+
5
+ import { IncomingMessage, ServerResponse } from "node:http";
6
+ import { ScratchContext } from "../../index";
7
+
8
+ export interface MiddlewareContext {
9
+ req: IncomingMessage | any;
10
+ res: ServerResponse | any;
11
+ context: ScratchContext & { [key: string]: any };
12
+ metadata: {
13
+ requestId?: string;
14
+ startTime?: number;
15
+ [key: string]: any;
16
+ };
17
+ }
18
+
19
+ export type Middleware = (
20
+ ctx: MiddlewareContext,
21
+ next: () => Promise<void>
22
+ ) => Promise<void> | void;
23
+
24
+ export interface MiddlewareResult {
25
+ handled: boolean;
26
+ }
27
+