@nullix/zod-mongoose-studio 3.3.0 → 3.3.2
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/.output/nitro.json +1 -1
- package/.output/public/_fonts/57NSSoFy1VLVs2gqly8Ls9awBnZMFyXGrefpmqvdqmc-zJfbBtpgM4cDmcXBsqZNW79_kFnlpPd62b48glgdydA.woff2 +0 -0
- package/.output/public/_fonts/8VR2wSMN-3U4NbWAVYXlkRV6hA0jFBXP-0RtL3X7fko-x2gYI4qfmkRdxyQQUPaBZdZdgl1TeVrquF_TxHeM4lM.woff2 +0 -0
- package/.output/public/_fonts/GsKUclqeNLJ96g5AU593ug6yanivOiwjW_7zESNPChw-jHA4tBeM1bjF7LATGUpfBuSTyomIFrWBTzjF7txVYfg.woff2 +0 -0
- package/.output/public/_fonts/Ld1FnTo3yTIwDyGfTQ5-Fws9AWsCbKfMvgxduXr7JcY-W25bL8NF1fjpLRSOgJb7RoZPHqGQNwMTM7S9tHVoxx8.woff2 +0 -0
- package/.output/public/_fonts/NdzqRASp2bovDUhQT1IRE_EMqKJ2KYQdTCfFcBvL8yw-KhwZiS86o3fErOe5GGMExHUemmI_dBfaEFxjISZrBd0.woff2 +0 -0
- package/.output/public/_fonts/iTkrULNFJJkTvihIg1Vqi5IODRH_9btXCioVF5l98I8-AndUyau2HR2felA_ra8V2mutQgschhasE5FD1dXGJX8.woff2 +0 -0
- package/.output/public/_nuxt/B6lqezyA.js +27 -0
- package/.output/public/_nuxt/{IxYCG8hB.js → B7ScFmDB.js} +1 -1
- package/.output/public/_nuxt/B8gqXD2U.js +1 -0
- package/.output/public/_nuxt/{ikxafTHA.js → BC9tFNFa.js} +180 -184
- package/.output/public/_nuxt/{qwxNOyg7.js → Bbi9UBvi.js} +1 -1
- package/.output/public/_nuxt/{CNZorjDf.js → BcYnCaqL.js} +1 -1
- package/.output/public/_nuxt/{BzhSDy30.js → BlZ0x31M.js} +1 -1
- package/.output/public/_nuxt/Bw82IGM8.js +1 -0
- package/.output/public/_nuxt/{qQyvCkbV.js → C8xmwXC2.js} +1 -1
- package/.output/public/_nuxt/{Ce-T_1aD.js → C919AVYR.js} +1 -1
- package/.output/public/_nuxt/{nDZOJ7rN.js → CsQL5F-l.js} +2 -2
- package/.output/public/_nuxt/Czp4_2o-.js +33 -0
- package/.output/public/_nuxt/{CC3n3Aga.js → DJa7_7Hz.js} +1 -1
- package/.output/public/_nuxt/DKK0Mrrg.js +1 -0
- package/.output/public/_nuxt/DbUZZZZu.js +6 -0
- package/.output/public/_nuxt/{B7P0wQ35.js → DbqvVYbv.js} +1 -1
- package/.output/public/_nuxt/{Ick_23I4.js → JkiDMdYG.js} +1 -1
- package/.output/public/_nuxt/{81vqh61j.js → OVQ3C2Ge.js} +1 -1
- package/.output/public/_nuxt/{Cs8GXJCB.js → PXBhK7S_.js} +1 -1
- package/.output/public/_nuxt/{CAd-qHfI.js → YC-RLggm.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/46174b5e-cab6-4a73-95f8-66a1c9b1be2b.json +1 -0
- package/.output/public/_nuxt/{tzj4nvDE.js → d1NBz903.js} +2 -2
- package/.output/public/_nuxt/{entry.J_CbC0oY.css → entry.DaqrxXuH.css} +1 -1
- package/.output/public/_nuxt/error-404.CTSXlFYa.css +1 -0
- package/.output/public/_nuxt/error-500.DqEfQnVZ.css +1 -0
- package/.output/public/_nuxt/{Bd32GOUy.js → iGzUozbF.js} +1 -1
- package/.output/public/_nuxt/{DjPT-NCE.js → iKW_mt0p.js} +1 -1
- package/.output/server/chunks/_/error-500.mjs.map +1 -1
- package/.output/server/chunks/build/client.precomputed.mjs +1 -1
- package/.output/server/chunks/build/{error-404-BN-7rNE4.mjs → error-404-KRkA0DPI.mjs} +10 -4
- package/.output/server/chunks/build/error-404-KRkA0DPI.mjs.map +1 -0
- package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs +8 -0
- package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs.map +1 -0
- package/.output/server/chunks/build/{error-500-DhIQ-2yt.mjs → error-500-DIIFf55p.mjs} +10 -4
- package/.output/server/chunks/build/error-500-DIIFf55p.mjs.map +1 -0
- package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs +8 -0
- package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs.map +1 -0
- package/.output/server/chunks/build/index-CB1N4gax.mjs +557 -0
- package/.output/server/chunks/build/index-CB1N4gax.mjs.map +1 -0
- package/.output/server/chunks/build/server.mjs +326 -248
- package/.output/server/chunks/build/server.mjs.map +1 -1
- package/.output/server/chunks/build/styles.mjs +4 -4
- package/.output/server/chunks/nitro/nitro.mjs +2319 -852
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/editor-types.get.mjs +113 -0
- package/.output/server/chunks/routes/api/editor-types.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/parse.post.mjs +26 -1448
- package/.output/server/chunks/routes/api/parse.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/resolve.get.mjs +6 -0
- package/.output/server/chunks/routes/api/resolve.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/studio/run.post.mjs +40 -0
- package/.output/server/chunks/routes/api/studio/run.post.mjs.map +1 -0
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs.map +1 -1
- package/.output/server/index.mjs +7 -1
- package/.output/server/index.mjs.map +1 -1
- package/.output/server/node_modules/acorn/dist/acorn.mjs +6266 -0
- package/.output/server/node_modules/acorn/package.json +50 -0
- package/.output/server/node_modules/ip-address/dist/common.js +112 -4
- package/.output/server/node_modules/ip-address/dist/ipv4.js +322 -87
- package/.output/server/node_modules/ip-address/dist/ipv6.js +654 -199
- package/.output/server/node_modules/ip-address/dist/v4/constants.js +45 -2
- package/.output/server/node_modules/ip-address/dist/v6/constants.js +55 -3
- package/.output/server/node_modules/ip-address/dist/v6/helpers.js +12 -3
- package/.output/server/node_modules/ip-address/package.json +49 -37
- package/.output/server/node_modules/ufo/dist/index.mjs +2 -2
- package/.output/server/node_modules/ufo/package.json +9 -9
- package/.output/server/package.json +4 -3
- package/README.md +65 -20
- package/app/assets/css/main.css +1 -0
- package/app/assets/css/studio.css +131 -0
- package/app/components/StudioConversion.vue +41 -0
- package/app/components/StudioDataPanel.vue +21 -0
- package/app/components/StudioEditor.vue +78 -102
- package/app/components/StudioExpandButton.vue +11 -0
- package/app/components/StudioOutput.vue +7 -1
- package/app/components/StudioSourcePanel.vue +16 -0
- package/app/components/StudioValidation.vue +47 -0
- package/app/components/ZodMongooseStudio.vue +216 -198
- package/app/composables/useStudioRunner.ts +142 -0
- package/app/pages/index.vue +1 -1
- package/app/plugins/monaco.client.ts +2 -0
- package/bin/index.mjs +5 -0
- package/nuxt.config.ts +4 -3
- package/package.json +6 -3
- package/server/api/editor-types.get.ts +7 -0
- package/server/api/parse.post.ts +17 -175
- package/server/api/studio/run.post.ts +29 -0
- package/server/middleware/security.ts +29 -7
- package/server/utils/editor-types.json +1 -0
- package/server/utils/example-data.ts +80 -0
- package/server/utils/parse-request.ts +28 -0
- package/server/utils/runner-client.ts +40 -0
- package/server/utils/studio-execution.ts +176 -0
- package/.output/public/_nuxt/BD40YYqZ.js +0 -33
- package/.output/public/_nuxt/BlQmLjV4.js +0 -1
- package/.output/public/_nuxt/CJ4Cv9fY.js +0 -61
- package/.output/public/_nuxt/builds/meta/495a8ebb-444d-4414-9b98-c181aa84d817.json +0 -1
- package/.output/public/_nuxt/error-404.64pZEiDn.css +0 -1
- package/.output/public/_nuxt/error-500.DHOpI0Ir.css +0 -1
- package/.output/public/_nuxt/rH_O4qXT.js +0 -1
- package/.output/server/chunks/build/error-404-BN-7rNE4.mjs.map +0 -1
- package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs +0 -8
- package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs.map +0 -1
- package/.output/server/chunks/build/error-500-DhIQ-2yt.mjs.map +0 -1
- package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs +0 -8
- package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs.map +0 -1
- package/.output/server/chunks/build/index-B4T6zHp0.mjs +0 -1904
- package/.output/server/chunks/build/index-B4T6zHp0.mjs.map +0 -1
- package/.output/server/node_modules/zod/index.js +0 -4
- /package/.output/public/_nuxt/{editor.CdBKkuhR.css → monaco.CdBKkuhR.css} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullix/zod-mongoose-studio",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "Monaco-powered TypeScript playground for @nullix/zod-mongoose",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"bin": {
|
|
@@ -39,12 +39,13 @@
|
|
|
39
39
|
"provenance": true
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"acorn": "8.16.0",
|
|
42
43
|
"esbuild": "0.28.0",
|
|
43
44
|
"get-port-please": "3.2.0",
|
|
44
45
|
"mongoose": "9.6.3",
|
|
45
46
|
"open": "11.0.0",
|
|
46
47
|
"zod": "4.3.6",
|
|
47
|
-
"@nullix/zod-mongoose": "3.3.
|
|
48
|
+
"@nullix/zod-mongoose": "3.3.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@nuxt/ui": "4.6.0",
|
|
@@ -57,10 +58,12 @@
|
|
|
57
58
|
"vue-tsc": "3.2.6"
|
|
58
59
|
},
|
|
59
60
|
"scripts": {
|
|
60
|
-
"
|
|
61
|
+
"generate:editor-types": "node scripts/generate-editor-types.mjs",
|
|
62
|
+
"build": "nuxt build",
|
|
61
63
|
"dev": "nuxt dev -p 34003",
|
|
62
64
|
"generate": "nuxt generate",
|
|
63
65
|
"preview": "nuxt preview",
|
|
66
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
64
67
|
"postinstall": "nuxt prepare"
|
|
65
68
|
}
|
|
66
69
|
}
|
package/server/api/parse.post.ts
CHANGED
|
@@ -1,187 +1,29 @@
|
|
|
1
|
-
import { defineEventHandler,
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import util from "node:util";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { defineEventHandler, createError } from "h3";
|
|
2
|
+
import { executeStudio, StudioExecutionError } from "../utils/studio-execution";
|
|
3
|
+
import { readStudioRequest } from "../utils/parse-request";
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
import * as mongoose from "mongoose";
|
|
10
|
-
import * as mongooseZod from "@nullix/zod-mongoose";
|
|
11
|
-
|
|
12
|
-
// Initialize Mongoose in ESM environment
|
|
13
|
-
mongooseZod.setMongoose(mongoose.default || mongoose);
|
|
14
|
-
|
|
15
|
-
// Shim __filename and __dirname for ESM environments (Nitro)
|
|
16
|
-
let _filename = "/sandbox/main.ts";
|
|
17
|
-
let _dirname = "/sandbox";
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
_filename = fileURLToPath(import.meta.url);
|
|
21
|
-
_dirname = dirname(_filename);
|
|
22
|
-
} catch {
|
|
23
|
-
// Fallback if import.meta.url is not available or valid in the current context
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// The converter exposes a module-level Hookable instance. User code can add
|
|
27
|
-
// hooks to it, so parse requests must not overlap and must always clean it up
|
|
28
|
-
// before the next request starts.
|
|
5
|
+
const MAX_PENDING_PARSES = 3;
|
|
29
6
|
let parseQueue = Promise.resolve();
|
|
7
|
+
let pendingParses = 0;
|
|
30
8
|
|
|
31
|
-
async function
|
|
9
|
+
async function acquireLocalLock(): Promise<() => void> {
|
|
10
|
+
if (pendingParses >= MAX_PENDING_PARSES) throw createError({ statusCode: 503, message: "Studio is busy. Try again shortly." });
|
|
11
|
+
pendingParses++;
|
|
32
12
|
const previous = parseQueue;
|
|
33
13
|
let release!: () => void;
|
|
34
|
-
|
|
35
|
-
parseQueue = new Promise<void>((resolve) => {
|
|
36
|
-
release = resolve;
|
|
37
|
-
});
|
|
38
|
-
|
|
14
|
+
parseQueue = new Promise<void>((resolve) => { release = () => { pendingParses--; resolve(); }; });
|
|
39
15
|
await previous;
|
|
40
16
|
return release;
|
|
41
17
|
}
|
|
42
18
|
|
|
43
|
-
function getErrorMessage(error: unknown): string {
|
|
44
|
-
if (error instanceof Error) return error.message || error.name;
|
|
45
|
-
return String(error);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// @ts-ignore
|
|
49
|
-
if (typeof __filename === "undefined") {
|
|
50
|
-
// @ts-ignore
|
|
51
|
-
globalThis.__filename = _filename;
|
|
52
|
-
}
|
|
53
|
-
// @ts-ignore
|
|
54
|
-
if (typeof __dirname === "undefined") {
|
|
55
|
-
// @ts-ignore
|
|
56
|
-
globalThis.__dirname = _dirname;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
19
|
export default defineEventHandler(async (event) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const { sourceCode } = await readBody(event);
|
|
63
|
-
|
|
64
|
-
if (!sourceCode) {
|
|
65
|
-
throw createError({ statusCode: 400, message: "Missing sourceCode" });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const releaseParseLock = await acquireParseLock();
|
|
69
|
-
|
|
70
|
-
// 1. Transpile exactly what the user wrote
|
|
71
|
-
try {
|
|
72
|
-
// A user hook can throw before the converter has completed. Since hooks are
|
|
73
|
-
// global within the server process, cleanup must happen for both success and
|
|
74
|
-
// failure, otherwise the user's hook would affect every later visitor.
|
|
75
|
-
mongooseZod.hooks.removeAllHooks();
|
|
76
|
-
|
|
77
|
-
let jsCode: string;
|
|
78
|
-
try {
|
|
79
|
-
jsCode = transformSync(sourceCode, {
|
|
80
|
-
loader: "ts",
|
|
81
|
-
format: "cjs",
|
|
82
|
-
target: "esnext",
|
|
83
|
-
// Provide these globals during transpilation just in case
|
|
84
|
-
define: {
|
|
85
|
-
__filename: '"/sandbox/main.ts"',
|
|
86
|
-
__dirname: '"/sandbox"',
|
|
87
|
-
},
|
|
88
|
-
}).code;
|
|
89
|
-
} catch (error: unknown) {
|
|
90
|
-
throw createError({
|
|
91
|
-
statusCode: 400,
|
|
92
|
-
message: `Compilation Error: ${getErrorMessage(error)}`,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
// 2. Map imports securely
|
|
96
|
-
const sandboxRequire = (moduleName: string) => {
|
|
97
|
-
if (moduleName === "zod" || moduleName === "zod/v4") return zod;
|
|
98
|
-
if (moduleName === "@nullix/zod-mongoose") return mongooseZod;
|
|
99
|
-
if (moduleName === "mongoose") return mongoose.default || mongoose;
|
|
100
|
-
throw new Error(`Security Exception: Module "${moduleName}" is blocked.`);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const sandbox = {
|
|
104
|
-
require: sandboxRequire,
|
|
105
|
-
console: { log: () => {}, warn: () => {}, error: () => {} },
|
|
106
|
-
__filename: "/sandbox/main.ts",
|
|
107
|
-
__dirname: "/sandbox",
|
|
108
|
-
};
|
|
109
|
-
const context = vm.createContext(sandbox);
|
|
110
|
-
|
|
111
|
-
const script = new vm.Script(`
|
|
112
|
-
((__filename, __dirname) => {
|
|
113
|
-
let exports = {};
|
|
114
|
-
let module = { exports };
|
|
115
|
-
|
|
116
|
-
${jsCode}
|
|
117
|
-
|
|
118
|
-
return module.exports;
|
|
119
|
-
})("/sandbox/main.ts", "/sandbox");
|
|
120
|
-
`);
|
|
121
|
-
|
|
122
|
-
// 3. Execute code and grab the default export
|
|
123
|
-
const rawExport = script.runInContext(context, {
|
|
124
|
-
timeout: isDocsMode ? 1000 : 5000,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const schema = rawExport.default || rawExport;
|
|
128
|
-
|
|
129
|
-
if (!schema || !schema._def) {
|
|
130
|
-
throw new Error("You must `export default` a valid Zod schema.");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 4. Run BOTH functions on the user's schema
|
|
134
|
-
const extractedDef = mongooseZod.extractMongooseDef(schema);
|
|
135
|
-
const mongooseSchema = mongooseZod.toMongooseSchema(schema, { modelName: 'ParsedSchema' });
|
|
136
|
-
|
|
137
|
-
// Format the outputs nicely
|
|
138
|
-
const formatOpts = { depth: null, showHidden: false, colors: false };
|
|
139
|
-
|
|
140
|
-
// Mongoose schemas are massive class instances, so we extract the raw .obj definition to display
|
|
141
|
-
const formatValue = (val: any): any => {
|
|
142
|
-
if (typeof val === "function") {
|
|
143
|
-
return val.name || val.toString();
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(val)) {
|
|
146
|
-
return val.map(formatValue);
|
|
147
|
-
}
|
|
148
|
-
if (
|
|
149
|
-
val !== null &&
|
|
150
|
-
typeof val === "object" &&
|
|
151
|
-
val.constructor === Object
|
|
152
|
-
) {
|
|
153
|
-
const formatted: any = {};
|
|
154
|
-
for (const key in val) {
|
|
155
|
-
formatted[key] = formatValue(val[key]);
|
|
156
|
-
}
|
|
157
|
-
return formatted;
|
|
158
|
-
}
|
|
159
|
-
return val;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const formattedDef = formatValue(extractedDef);
|
|
163
|
-
// schemaObj is a Mongoose Schema instance, we only care about its .obj for display
|
|
164
|
-
const formattedSchema = formatValue(mongooseSchema.obj);
|
|
165
|
-
// @ts-ignore
|
|
166
|
-
const formattedTree = formatValue(mongooseSchema.tree);
|
|
167
|
-
// @ts-ignore
|
|
168
|
-
const formattedPaths = formatValue(mongooseSchema.paths);
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
definition: util.inspect(formattedDef, formatOpts),
|
|
172
|
-
schemaObj: util.inspect(formattedSchema, formatOpts),
|
|
173
|
-
schemaTree: util.inspect(formattedTree, formatOpts),
|
|
174
|
-
schemaPaths: util.inspect(formattedPaths, formatOpts),
|
|
175
|
-
};
|
|
176
|
-
} catch (err: any) {
|
|
177
|
-
const message = getErrorMessage(err);
|
|
178
|
-
const isCompilationError = err?.statusCode === 400;
|
|
179
|
-
throw createError({
|
|
180
|
-
statusCode: isCompilationError ? 400 : 500,
|
|
181
|
-
message: isCompilationError ? message : `Execution Error: ${message}`,
|
|
182
|
-
});
|
|
183
|
-
} finally {
|
|
184
|
-
mongooseZod.hooks.removeAllHooks();
|
|
185
|
-
releaseParseLock();
|
|
20
|
+
if (useRuntimeConfig(event).public.isDocsMode) {
|
|
21
|
+
throw createError({ statusCode: 403, message: "Local Studio execution is unavailable in docs mode." });
|
|
186
22
|
}
|
|
23
|
+
const body = await readStudioRequest(event);
|
|
24
|
+
const release = await acquireLocalLock();
|
|
25
|
+
try { return executeStudio(body, 5000); }
|
|
26
|
+
catch (error: any) {
|
|
27
|
+
throw createError({ statusCode: error instanceof StudioExecutionError ? 400 : 500, message: error?.message || "Studio execution failed." });
|
|
28
|
+
} finally { release(); }
|
|
187
29
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createError, defineEventHandler } from "h3";
|
|
2
|
+
import { readStudioRequest } from "../../utils/parse-request";
|
|
3
|
+
import { runInContainer } from "../../utils/runner-client";
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
if (!useRuntimeConfig(event).public.isDocsMode) {
|
|
7
|
+
throw createError({
|
|
8
|
+
statusCode: 403,
|
|
9
|
+
message: "Use the local Studio endpoint in CLI mode.",
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
const socketPath =
|
|
13
|
+
process.env.STUDIO_RUNNER_SOCKET || "/run/zod-studio/runner.sock";
|
|
14
|
+
if (!socketPath)
|
|
15
|
+
throw createError({
|
|
16
|
+
statusCode: 503,
|
|
17
|
+
message: "Studio runner is not configured.",
|
|
18
|
+
});
|
|
19
|
+
const body = await readStudioRequest(event);
|
|
20
|
+
try {
|
|
21
|
+
return await runInContainer(body, socketPath);
|
|
22
|
+
} catch (error: any) {
|
|
23
|
+
if (error?.statusCode) throw error;
|
|
24
|
+
throw createError({
|
|
25
|
+
statusCode: 503,
|
|
26
|
+
message: "Studio runner is unavailable. Try again shortly.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { defineEventHandler, createError, getRequestIP } from "h3";
|
|
1
|
+
import { defineEventHandler, createError, getHeader, getRequestIP, setHeader } from "h3";
|
|
2
2
|
|
|
3
3
|
// Simple in-memory rate limiting map: IP -> array of timestamps
|
|
4
4
|
const rateLimitMap = new Map<string, number[]>();
|
|
5
|
-
const RATE_LIMIT =
|
|
5
|
+
const RATE_LIMIT = 8; // Allow quick edits and shortcut runs without throttling normal use.
|
|
6
6
|
const TIME_WINDOW = 1000; // per 1 second (1000ms)
|
|
7
|
+
const GLOBAL_RATE_LIMIT = 20;
|
|
8
|
+
let globalWindowStart = 0;
|
|
9
|
+
let globalRequests = 0;
|
|
7
10
|
|
|
8
11
|
export default defineEventHandler((event) => {
|
|
9
|
-
const isDocsMode =
|
|
12
|
+
const isDocsMode = useRuntimeConfig(event).public.isDocsMode;
|
|
10
13
|
const path = event.path;
|
|
11
14
|
|
|
15
|
+
if (!isDocsMode && path.startsWith("/api/")) {
|
|
16
|
+
const host = getHeader(event, "host") || "";
|
|
17
|
+
const origin = getHeader(event, "origin");
|
|
18
|
+
const localHost = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(host);
|
|
19
|
+
const localOrigin = !origin || /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
|
|
20
|
+
if (!localHost || !localOrigin) throw createError({ statusCode: 403, message: "Local Studio accepts only loopback requests." });
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
// 1. Environment Check: Disable local resolving in Docs mode
|
|
13
24
|
if (isDocsMode && path.startsWith("/api/resolve")) {
|
|
14
25
|
throw createError({
|
|
@@ -17,8 +28,8 @@ export default defineEventHandler((event) => {
|
|
|
17
28
|
});
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
// 2. Rate
|
|
21
|
-
if (isDocsMode && path
|
|
31
|
+
// 2. Rate limiting for the parse endpoint in Docs Mode
|
|
32
|
+
if (isDocsMode && path === "/api/studio/run") {
|
|
22
33
|
const ip = getRequestIP(event) || "unknown";
|
|
23
34
|
const now = Date.now();
|
|
24
35
|
|
|
@@ -27,14 +38,25 @@ export default defineEventHandler((event) => {
|
|
|
27
38
|
// Filter timestamps within the current window
|
|
28
39
|
const recentRequests = timestamps.filter((t) => now - t < TIME_WINDOW);
|
|
29
40
|
|
|
30
|
-
if (
|
|
41
|
+
if (now - globalWindowStart >= TIME_WINDOW) {
|
|
42
|
+
globalWindowStart = now;
|
|
43
|
+
globalRequests = 0;
|
|
44
|
+
}
|
|
45
|
+
if (rateLimitMap.size > 1024) {
|
|
46
|
+
for (const [key, entries] of rateLimitMap) {
|
|
47
|
+
if (entries.every((time) => now - time >= TIME_WINDOW)) rateLimitMap.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (recentRequests.length >= RATE_LIMIT || globalRequests >= GLOBAL_RATE_LIMIT || (rateLimitMap.size >= 1024 && !rateLimitMap.has(ip))) {
|
|
51
|
+
setHeader(event, "Retry-After", 1);
|
|
31
52
|
throw createError({
|
|
32
53
|
statusCode: 429,
|
|
33
|
-
message: "Too
|
|
54
|
+
message: "Too many runs. Wait a moment and try again.",
|
|
34
55
|
});
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
recentRequests.push(now);
|
|
38
59
|
rateLimitMap.set(ip, recentRequests);
|
|
60
|
+
globalRequests++;
|
|
39
61
|
}
|
|
40
62
|
});
|