@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
|
@@ -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
|
+
|