@quonfig/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +9 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/api/get-environments.d.ts +10 -0
- package/dist/api/get-environments.js +11 -0
- package/dist/codegen/code-generators/base-generator.d.ts +11 -0
- package/dist/codegen/code-generators/base-generator.js +8 -0
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +17 -0
- package/dist/codegen/code-generators/base-typescript-generator.js +35 -0
- package/dist/codegen/code-generators/node-typescript-generator.d.ts +8 -0
- package/dist/codegen/code-generators/node-typescript-generator.js +96 -0
- package/dist/codegen/code-generators/react-typescript-generator.d.ts +11 -0
- package/dist/codegen/code-generators/react-typescript-generator.js +116 -0
- package/dist/codegen/config-downloader.d.ts +7 -0
- package/dist/codegen/config-downloader.js +26 -0
- package/dist/codegen/json-schema-to-zod.d.ts +3 -0
- package/dist/codegen/json-schema-to-zod.js +170 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.d.ts +4 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.js +55 -0
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-base-mapper.js +111 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +25 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +87 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +133 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +36 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +150 -0
- package/dist/codegen/mustache-extractor.d.ts +6 -0
- package/dist/codegen/mustache-extractor.js +83 -0
- package/dist/codegen/schema-evaluator.d.ts +37 -0
- package/dist/codegen/schema-evaluator.js +283 -0
- package/dist/codegen/schema-extractor.d.ts +24 -0
- package/dist/codegen/schema-extractor.js +265 -0
- package/dist/codegen/types.d.ts +44 -0
- package/dist/codegen/types.js +5 -0
- package/dist/codegen/zod-introspection.d.ts +85 -0
- package/dist/codegen/zod-introspection.js +205 -0
- package/dist/commands/create.d.ts +19 -0
- package/dist/commands/create.js +175 -0
- package/dist/commands/download.d.ts +13 -0
- package/dist/commands/download.js +63 -0
- package/dist/commands/generate-new-hex-key.d.ts +7 -0
- package/dist/commands/generate-new-hex-key.js +10 -0
- package/dist/commands/generate.d.ts +16 -0
- package/dist/commands/generate.js +220 -0
- package/dist/commands/get.d.ts +13 -0
- package/dist/commands/get.js +141 -0
- package/dist/commands/info.d.ts +21 -0
- package/dist/commands/info.js +464 -0
- package/dist/commands/interactive.d.ts +5 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/list.d.ts +13 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +145 -0
- package/dist/commands/override.d.ts +17 -0
- package/dist/commands/override.js +170 -0
- package/dist/commands/profile.d.ts +7 -0
- package/dist/commands/profile.js +47 -0
- package/dist/commands/schema.d.ts +21 -0
- package/dist/commands/schema.js +168 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +99 -0
- package/dist/commands/set-default.d.ts +20 -0
- package/dist/commands/set-default.js +273 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +48 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/commands/workspace.d.ts +7 -0
- package/dist/commands/workspace.js +38 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +10 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +112 -0
- package/dist/interactive-prompt.d.ts +2 -0
- package/dist/interactive-prompt.js +162 -0
- package/dist/quonfig.d.ts +15 -0
- package/dist/quonfig.js +75 -0
- package/dist/result.d.ts +35 -0
- package/dist/result.js +9 -0
- package/dist/services/mcp.d.ts +15 -0
- package/dist/services/mcp.js +59 -0
- package/dist/ui/get-confirmation.d.ts +10 -0
- package/dist/ui/get-confirmation.js +4 -0
- package/dist/ui/get-environment.d.ts +11 -0
- package/dist/ui/get-environment.js +42 -0
- package/dist/ui/get-key.d.ts +27 -0
- package/dist/ui/get-key.js +15 -0
- package/dist/ui/get-string.d.ts +5 -0
- package/dist/ui/get-string.js +8 -0
- package/dist/ui/get-value.d.ts +18 -0
- package/dist/ui/get-value.js +46 -0
- package/dist/util/autocomplete.d.ts +6 -0
- package/dist/util/autocomplete.js +25 -0
- package/dist/util/coerce.d.ts +7 -0
- package/dist/util/coerce.js +62 -0
- package/dist/util/color.d.ts +3 -0
- package/dist/util/color.js +4 -0
- package/dist/util/config-value-dto.d.ts +16 -0
- package/dist/util/config-value-dto.js +82 -0
- package/dist/util/context.d.ts +18 -0
- package/dist/util/context.js +10 -0
- package/dist/util/domain-urls.d.ts +3 -0
- package/dist/util/domain-urls.js +18 -0
- package/dist/util/encryption.d.ts +20 -0
- package/dist/util/encryption.js +153 -0
- package/dist/util/file-manager.d.ts +12 -0
- package/dist/util/file-manager.js +16 -0
- package/dist/util/get-client.d.ts +7 -0
- package/dist/util/get-client.js +127 -0
- package/dist/util/is-interactive.d.ts +5 -0
- package/dist/util/is-interactive.js +7 -0
- package/dist/util/json-maybe.d.ts +3 -0
- package/dist/util/json-maybe.js +14 -0
- package/dist/util/name-arg.d.ts +4 -0
- package/dist/util/name-arg.js +3 -0
- package/dist/util/oauth-client.d.ts +30 -0
- package/dist/util/oauth-client.js +99 -0
- package/dist/util/secret-flags.d.ts +13 -0
- package/dist/util/secret-flags.js +13 -0
- package/dist/util/token-storage.d.ts +25 -0
- package/dist/util/token-storage.js +90 -0
- package/dist/validations/value.d.ts +4 -0
- package/dist/validations/value.js +11 -0
- package/dist/verify/standalone.d.ts +14 -0
- package/dist/verify/standalone.js +133 -0
- package/dist/verify/validate.d.ts +43 -0
- package/dist/verify/validate.js +756 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/oclif.manifest.json +1429 -0
- package/package.json +104 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ZodTypeSupported } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the secure schema validator
|
|
4
|
+
*/
|
|
5
|
+
export interface SecureSchemaValidatorOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Whether to perform AST validation
|
|
8
|
+
*/
|
|
9
|
+
astValidation?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum allowed AST nodes to prevent DoS attacks
|
|
12
|
+
*/
|
|
13
|
+
maxAstNodes?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates a JavaScript expression AST for potentially unsafe operations
|
|
17
|
+
*
|
|
18
|
+
* @param ast - The AST to validate
|
|
19
|
+
* @param parentMap - Map of nodes to their parent nodes
|
|
20
|
+
* @returns An object with validation result and optional error message
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateAst(ast: any, parentMap: WeakMap<any, any>): {
|
|
23
|
+
error?: string;
|
|
24
|
+
isValid: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Securely evaluates a Zod schema string using AST validation
|
|
28
|
+
*
|
|
29
|
+
* @param schemaString - The schema string to evaluate
|
|
30
|
+
* @param options - Options for secure evaluation
|
|
31
|
+
* @returns The evaluated schema or an error
|
|
32
|
+
*/
|
|
33
|
+
export declare function secureEvaluateSchema(schemaString: string, options?: SecureSchemaValidatorOptions): {
|
|
34
|
+
error?: string;
|
|
35
|
+
schema?: ZodTypeSupported;
|
|
36
|
+
success: boolean;
|
|
37
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// TODO: We should resolve these any references to ensure generation is fully type-safe
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import * as acorn from 'acorn';
|
|
4
|
+
import * as walk from 'acorn-walk';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
/**
|
|
7
|
+
* Default options for the secure schema validator
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
astValidation: true,
|
|
11
|
+
maxAstNodes: 500,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Validates a JavaScript expression AST for potentially unsafe operations
|
|
15
|
+
*
|
|
16
|
+
* @param ast - The AST to validate
|
|
17
|
+
* @param parentMap - Map of nodes to their parent nodes
|
|
18
|
+
* @returns An object with validation result and optional error message
|
|
19
|
+
*/
|
|
20
|
+
export function validateAst(ast, parentMap) {
|
|
21
|
+
let isValid = true;
|
|
22
|
+
let error;
|
|
23
|
+
// Track what we've seen for detailed error messages
|
|
24
|
+
const issues = [];
|
|
25
|
+
// Count nodes to prevent DoS attacks with extremely large schemas
|
|
26
|
+
let nodeCount = 0;
|
|
27
|
+
// Helper function to check if an arrow function is used in a valid Zod method context
|
|
28
|
+
const isValidZodMethodArg = (node) => {
|
|
29
|
+
// Check if this node is an argument to a method call
|
|
30
|
+
const parent = parentMap.get(node);
|
|
31
|
+
if (parent && parent.type === 'CallExpression' && parent.callee && parent.callee.type === 'MemberExpression') {
|
|
32
|
+
// Check if method is on an object that's part of a valid Zod chain
|
|
33
|
+
const memberExpr = parent.callee;
|
|
34
|
+
// Allow methods like refine, transform, superRefine, etc.
|
|
35
|
+
const allowedMethods = ['refine', 'transform', 'superRefine', 'pipe', 'preprocess'];
|
|
36
|
+
if (memberExpr.property && memberExpr.property.name && allowedMethods.includes(memberExpr.property.name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
// Count all nodes separately to track complexity
|
|
43
|
+
walk.full(ast, () => {
|
|
44
|
+
nodeCount++;
|
|
45
|
+
});
|
|
46
|
+
// Custom AST walker to check for unsafe patterns
|
|
47
|
+
walk.simple(ast, {
|
|
48
|
+
ArrowFunctionExpression(node) {
|
|
49
|
+
// Allow arrow functions only in valid Zod method contexts
|
|
50
|
+
if (!isValidZodMethodArg(node)) {
|
|
51
|
+
issues.push(`Forbidden standalone arrow function`);
|
|
52
|
+
isValid = false;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
// Block async/await
|
|
56
|
+
AwaitExpression() {
|
|
57
|
+
issues.push('Await expressions are not allowed');
|
|
58
|
+
isValid = false;
|
|
59
|
+
},
|
|
60
|
+
// Restrict function calls
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (node.callee.type === 'MemberExpression') {
|
|
63
|
+
const obj = node.callee.object;
|
|
64
|
+
if (obj.type === 'Identifier' && obj.name !== 'z') {
|
|
65
|
+
issues.push(`Only calls to z.* methods are allowed, found: ${obj.name}`);
|
|
66
|
+
isValid = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
|
|
70
|
+
issues.push(`Forbidden function call: require`);
|
|
71
|
+
isValid = false;
|
|
72
|
+
}
|
|
73
|
+
else if (node.callee.type === 'Identifier' && !['z'].includes(node.callee.name)) {
|
|
74
|
+
issues.push(`Forbidden function call: ${node.callee.name}`);
|
|
75
|
+
isValid = false;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
// Block class declarations
|
|
79
|
+
ClassDeclaration() {
|
|
80
|
+
issues.push(`Forbidden node type: ClassDeclaration`);
|
|
81
|
+
isValid = false;
|
|
82
|
+
},
|
|
83
|
+
ClassExpression() {
|
|
84
|
+
issues.push(`Forbidden node type: ClassExpression`);
|
|
85
|
+
isValid = false;
|
|
86
|
+
},
|
|
87
|
+
ExportAllDeclaration() {
|
|
88
|
+
issues.push(`Forbidden operation: ExportAllDeclaration`);
|
|
89
|
+
isValid = false;
|
|
90
|
+
},
|
|
91
|
+
ExportDefaultDeclaration() {
|
|
92
|
+
issues.push(`Forbidden operation: ExportDefaultDeclaration`);
|
|
93
|
+
isValid = false;
|
|
94
|
+
},
|
|
95
|
+
ExportNamedDeclaration() {
|
|
96
|
+
issues.push(`Forbidden operation: ExportNamedDeclaration`);
|
|
97
|
+
isValid = false;
|
|
98
|
+
},
|
|
99
|
+
// Block function declarations
|
|
100
|
+
FunctionDeclaration() {
|
|
101
|
+
issues.push(`Forbidden node type: FunctionDeclaration`);
|
|
102
|
+
isValid = false;
|
|
103
|
+
},
|
|
104
|
+
FunctionExpression() {
|
|
105
|
+
issues.push(`Forbidden node type: FunctionExpression`);
|
|
106
|
+
isValid = false;
|
|
107
|
+
},
|
|
108
|
+
// Block access to global objects and require
|
|
109
|
+
Identifier(node) {
|
|
110
|
+
const parent = parentMap.get(node);
|
|
111
|
+
const forbiddenGlobals = [
|
|
112
|
+
'window',
|
|
113
|
+
'global',
|
|
114
|
+
'process',
|
|
115
|
+
'console',
|
|
116
|
+
'document',
|
|
117
|
+
'navigator',
|
|
118
|
+
'localStorage',
|
|
119
|
+
'fetch',
|
|
120
|
+
'XMLHttpRequest',
|
|
121
|
+
'WebSocket',
|
|
122
|
+
'eval',
|
|
123
|
+
'require',
|
|
124
|
+
];
|
|
125
|
+
if (forbiddenGlobals.includes(node.name) && !(parent?.type === 'MemberExpression' && parent.property === node)) {
|
|
126
|
+
issues.push(`Forbidden global object: ${node.name}`);
|
|
127
|
+
isValid = false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
// Block imports, exports
|
|
131
|
+
ImportDeclaration() {
|
|
132
|
+
issues.push(`Forbidden operation: ImportDeclaration`);
|
|
133
|
+
isValid = false;
|
|
134
|
+
},
|
|
135
|
+
// Block dangerous property access
|
|
136
|
+
MemberExpression(node) {
|
|
137
|
+
if (node.property.type === 'Identifier') {
|
|
138
|
+
const dangerousProps = [
|
|
139
|
+
'constructor',
|
|
140
|
+
'prototype',
|
|
141
|
+
'__proto__',
|
|
142
|
+
'__defineGetter__',
|
|
143
|
+
'__defineSetter__',
|
|
144
|
+
'__lookupGetter__',
|
|
145
|
+
'__lookupSetter__',
|
|
146
|
+
];
|
|
147
|
+
if (dangerousProps.includes(node.property.name)) {
|
|
148
|
+
issues.push(`Forbidden property access: ${node.property.name}`);
|
|
149
|
+
isValid = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (nodeCount > DEFAULT_OPTIONS.maxAstNodes) {
|
|
155
|
+
isValid = false;
|
|
156
|
+
issues.push(`Schema exceeds maximum allowed complexity (${nodeCount} nodes > ${DEFAULT_OPTIONS.maxAstNodes} limit)`);
|
|
157
|
+
}
|
|
158
|
+
if (!isValid) {
|
|
159
|
+
error = `Schema contains potentially unsafe operations: ${issues.join(', ')}`;
|
|
160
|
+
}
|
|
161
|
+
return { error, isValid };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Removes .describe() calls from the AST by filtering them out
|
|
165
|
+
* This is more robust than regex as it properly handles nested structures
|
|
166
|
+
*
|
|
167
|
+
* @param ast - The AST to process
|
|
168
|
+
* @param schemaString - The original schema string
|
|
169
|
+
* @returns The schema string with .describe() calls removed
|
|
170
|
+
*/
|
|
171
|
+
function removeDescribeCalls(ast, schemaString) {
|
|
172
|
+
const nodesToRemove = [];
|
|
173
|
+
// Walk the AST and find all .describe() calls
|
|
174
|
+
walk.simple(ast, {
|
|
175
|
+
CallExpression(node) {
|
|
176
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
177
|
+
node.callee.property.type === 'Identifier' &&
|
|
178
|
+
node.callee.property.name === 'describe') {
|
|
179
|
+
// Mark the entire .describe(...) call for removal
|
|
180
|
+
// We want to remove from the dot before 'describe' to the closing paren
|
|
181
|
+
const start = node.callee.property.start - 1; // Include the dot
|
|
182
|
+
const end = node.end;
|
|
183
|
+
nodesToRemove.push({ start, end });
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
// Sort in reverse order so we can remove from the end without affecting earlier positions
|
|
188
|
+
nodesToRemove.sort((a, b) => b.start - a.start);
|
|
189
|
+
let result = schemaString;
|
|
190
|
+
for (const { start, end } of nodesToRemove) {
|
|
191
|
+
result = result.slice(0, Math.max(0, start)) + result.slice(Math.max(0, end));
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Securely evaluates a Zod schema string using AST validation
|
|
197
|
+
*
|
|
198
|
+
* @param schemaString - The schema string to evaluate
|
|
199
|
+
* @param options - Options for secure evaluation
|
|
200
|
+
* @returns The evaluated schema or an error
|
|
201
|
+
*/
|
|
202
|
+
export function secureEvaluateSchema(schemaString, options = {}) {
|
|
203
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
204
|
+
const trimmedSchema = schemaString.trim();
|
|
205
|
+
try {
|
|
206
|
+
// Phase 1: AST Validation (Static Analysis)
|
|
207
|
+
if (mergedOptions.astValidation) {
|
|
208
|
+
// Parse the schema string into an AST
|
|
209
|
+
const ast = acorn.parse(trimmedSchema, {
|
|
210
|
+
allowAwaitOutsideFunction: false,
|
|
211
|
+
ecmaVersion: 2020,
|
|
212
|
+
sourceType: 'script',
|
|
213
|
+
});
|
|
214
|
+
// Create a map to track parent-child relationships
|
|
215
|
+
const parentMap = new WeakMap();
|
|
216
|
+
// Function to build parent-child relationships
|
|
217
|
+
function buildParentChildRelationships(node, parent = null) {
|
|
218
|
+
if (parent) {
|
|
219
|
+
parentMap.set(node, parent);
|
|
220
|
+
}
|
|
221
|
+
// Recursively process all properties that might contain child nodes
|
|
222
|
+
for (const key in node) {
|
|
223
|
+
if (Object.hasOwn(node, key)) {
|
|
224
|
+
const child = node[key];
|
|
225
|
+
if (child && typeof child === 'object') {
|
|
226
|
+
if (Array.isArray(child)) {
|
|
227
|
+
for (const item of child) {
|
|
228
|
+
if (item && typeof item === 'object') {
|
|
229
|
+
buildParentChildRelationships(item, node);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
buildParentChildRelationships(child, node);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Build the parent-child relationships
|
|
241
|
+
buildParentChildRelationships(ast);
|
|
242
|
+
// Validate the AST for security concerns
|
|
243
|
+
const { error, isValid } = validateAst(ast, parentMap);
|
|
244
|
+
if (!isValid) {
|
|
245
|
+
return { error, success: false };
|
|
246
|
+
}
|
|
247
|
+
// Remove .describe() calls after validation
|
|
248
|
+
const filteredSchema = removeDescribeCalls(ast, trimmedSchema);
|
|
249
|
+
// Phase 2: Execute with Function constructor
|
|
250
|
+
// We're deliberately avoiding VM modules due to security concerns
|
|
251
|
+
// The AST validation provides our primary security layer
|
|
252
|
+
// eslint-disable-next-line no-new-func
|
|
253
|
+
const constructSchema = new Function('z', `return ${filteredSchema}`);
|
|
254
|
+
const schema = constructSchema(z);
|
|
255
|
+
// Phase 3: Validate Result
|
|
256
|
+
if (!schema || typeof schema !== 'object' || !('_def' in schema)) {
|
|
257
|
+
return {
|
|
258
|
+
error: 'The provided string did not evaluate to a valid Zod schema',
|
|
259
|
+
success: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { schema, success: true };
|
|
263
|
+
}
|
|
264
|
+
// If AST validation is disabled, just execute directly (not recommended)
|
|
265
|
+
// eslint-disable-next-line no-new-func
|
|
266
|
+
const constructSchema = new Function('z', `return ${trimmedSchema}`);
|
|
267
|
+
const schema = constructSchema(z);
|
|
268
|
+
// Phase 3: Validate Result
|
|
269
|
+
if (!schema || typeof schema !== 'object' || !('_def' in schema)) {
|
|
270
|
+
return {
|
|
271
|
+
error: 'The provided string did not evaluate to a valid Zod schema',
|
|
272
|
+
success: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return { schema, success: true };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
error: error instanceof Error ? `Evaluation error: ${error.message}` : 'Unknown evaluation error',
|
|
280
|
+
success: false,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type Config, type ConfigFile } from './types.js';
|
|
3
|
+
export type DurationTypeMap = () => z.ZodTypeAny;
|
|
4
|
+
export declare class SchemaExtractor {
|
|
5
|
+
private log;
|
|
6
|
+
constructor(log: (category: string | unknown, message?: unknown) => void);
|
|
7
|
+
execute({ config, configFile, durationTypeMap, }: {
|
|
8
|
+
config: Config;
|
|
9
|
+
configFile: ConfigFile;
|
|
10
|
+
durationTypeMap?: DurationTypeMap;
|
|
11
|
+
}): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
12
|
+
private createZodFunctionFromMustacheStrings;
|
|
13
|
+
private getAllJsonValues;
|
|
14
|
+
/**
|
|
15
|
+
* Get all string values at a specific location in the config
|
|
16
|
+
* @param config The configuration to extract strings from
|
|
17
|
+
* @param location Array of keys representing the path to look for strings. Empty array for direct string values.
|
|
18
|
+
* @returns Array of strings found at the specified location
|
|
19
|
+
*/
|
|
20
|
+
private getAllStringsAtLocation;
|
|
21
|
+
private inferFromConfig;
|
|
22
|
+
private replaceStringsWithMustache;
|
|
23
|
+
private resolveUserSchema;
|
|
24
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { jsonSchemaToZod, isLegacySchemaWrapper } from './json-schema-to-zod.js';
|
|
3
|
+
import { JsonToZodMapper } from './language-mappers/json-to-zod-mapper.js';
|
|
4
|
+
import { ZodToStringMapper } from './language-mappers/zod-to-string-mapper.js';
|
|
5
|
+
import { MustacheExtractor } from './mustache-extractor.js';
|
|
6
|
+
import { secureEvaluateSchema } from './schema-evaluator.js';
|
|
7
|
+
export class SchemaExtractor {
|
|
8
|
+
log;
|
|
9
|
+
constructor(log) {
|
|
10
|
+
this.log = log;
|
|
11
|
+
}
|
|
12
|
+
execute({ config, configFile, durationTypeMap, }) {
|
|
13
|
+
const userDefinedSchema = this.resolveUserSchema(config, configFile);
|
|
14
|
+
const schemaWithoutMustache = userDefinedSchema ?? this.inferFromConfig(config, durationTypeMap);
|
|
15
|
+
return this.replaceStringsWithMustache(schemaWithoutMustache, config);
|
|
16
|
+
}
|
|
17
|
+
createZodFunctionFromMustacheStrings(strings) {
|
|
18
|
+
if (strings.length === 0) {
|
|
19
|
+
return z.string();
|
|
20
|
+
}
|
|
21
|
+
if (strings.length > 1) {
|
|
22
|
+
// De-dup exact schemas
|
|
23
|
+
const schemas = {};
|
|
24
|
+
strings.forEach((str) => {
|
|
25
|
+
const schema = MustacheExtractor.extractSchema(str, this.log);
|
|
26
|
+
const stringSchema = new ZodToStringMapper().renderField(schema);
|
|
27
|
+
schemas[stringSchema] = schema;
|
|
28
|
+
});
|
|
29
|
+
const schemaResults = Object.values(schemas);
|
|
30
|
+
// If the schema is empty (no properties), just return basic string
|
|
31
|
+
const noSchemasPresent = schemaResults.every((s) => {
|
|
32
|
+
if (s instanceof z.ZodObject) {
|
|
33
|
+
return Object.keys(s.shape).length === 0;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
if (noSchemasPresent) {
|
|
38
|
+
return z.string();
|
|
39
|
+
}
|
|
40
|
+
// Return an explicit function if we have only one schema defined
|
|
41
|
+
if (schemaResults.length === 1) {
|
|
42
|
+
return z.function({ input: z.tuple([schemaResults[0]]), output: z.string() });
|
|
43
|
+
}
|
|
44
|
+
// Return a function with a union of schemas if we have multiple defined
|
|
45
|
+
return z.function({
|
|
46
|
+
input: z.tuple([z.union(schemaResults)]),
|
|
47
|
+
output: z.string(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const schema = MustacheExtractor.extractSchema(strings[0], this.log);
|
|
51
|
+
// If the schema is empty (no properties), just return basic string
|
|
52
|
+
if (schema instanceof z.ZodObject && Object.keys(schema.shape).length === 0) {
|
|
53
|
+
return z.string();
|
|
54
|
+
}
|
|
55
|
+
return z.function({ input: z.tuple([schema]), output: z.string() });
|
|
56
|
+
}
|
|
57
|
+
getAllJsonValues(config) {
|
|
58
|
+
return config.rows.flatMap((row) => row.values.flatMap((valueObj) => {
|
|
59
|
+
if (config.valueType === 'JSON') {
|
|
60
|
+
// Try to parse JSON from json field
|
|
61
|
+
if (valueObj.value.json?.json) {
|
|
62
|
+
try {
|
|
63
|
+
return [JSON.parse(valueObj.value.json.json)];
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.warn(`Failed to parse JSON for ${config.key}:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Try to parse JSON from string field
|
|
70
|
+
else if (valueObj.value.string) {
|
|
71
|
+
try {
|
|
72
|
+
return [JSON.parse(valueObj.value.string)];
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn(`Failed to parse JSON string for ${config.key}:`, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get all string values at a specific location in the config
|
|
84
|
+
* @param config The configuration to extract strings from
|
|
85
|
+
* @param location Array of keys representing the path to look for strings. Empty array for direct string values.
|
|
86
|
+
* @returns Array of strings found at the specified location
|
|
87
|
+
*/
|
|
88
|
+
getAllStringsAtLocation(config, location) {
|
|
89
|
+
if (location.length === 0) {
|
|
90
|
+
// For empty location, just get direct string values
|
|
91
|
+
return config.rows.flatMap((row) => row.values.flatMap((valueObj) => (valueObj.value.string ? [valueObj.value.string] : [])));
|
|
92
|
+
}
|
|
93
|
+
// For JSON values, we need to traverse the object
|
|
94
|
+
return config.rows.flatMap((row) => row.values.flatMap((valueObj) => {
|
|
95
|
+
let jsonContent = null;
|
|
96
|
+
// Try to parse JSON from either json field or string field
|
|
97
|
+
if (valueObj.value.json?.json) {
|
|
98
|
+
try {
|
|
99
|
+
jsonContent = JSON.parse(valueObj.value.json.json);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.warn(`Failed to parse JSON for ${config.key}:`, error);
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (valueObj.value.string && config.valueType === 'JSON') {
|
|
107
|
+
try {
|
|
108
|
+
jsonContent = JSON.parse(valueObj.value.string);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.warn(`Failed to parse JSON string for ${config.key}:`, error);
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!jsonContent)
|
|
116
|
+
return [];
|
|
117
|
+
// Traverse the JSON object to the specified location
|
|
118
|
+
let current = jsonContent;
|
|
119
|
+
for (const key of location) {
|
|
120
|
+
if (current && typeof current === 'object' && key in current) {
|
|
121
|
+
current = current[key];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// If we found a string at the location, return it
|
|
128
|
+
return typeof current === 'string' ? [current] : [];
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
inferFromConfig(config, durationTypeMap) {
|
|
132
|
+
const { key, valueType } = config;
|
|
133
|
+
switch (valueType) {
|
|
134
|
+
case 'STRING': {
|
|
135
|
+
return z.string();
|
|
136
|
+
}
|
|
137
|
+
case 'BOOL': {
|
|
138
|
+
return z.boolean();
|
|
139
|
+
}
|
|
140
|
+
case 'INT': {
|
|
141
|
+
return z.number().int();
|
|
142
|
+
}
|
|
143
|
+
case 'DOUBLE': {
|
|
144
|
+
return z.number();
|
|
145
|
+
}
|
|
146
|
+
case 'STRING_LIST': {
|
|
147
|
+
return z.array(z.string());
|
|
148
|
+
}
|
|
149
|
+
case 'DURATION': {
|
|
150
|
+
return durationTypeMap?.() ?? z.number();
|
|
151
|
+
}
|
|
152
|
+
case 'JSON': {
|
|
153
|
+
const jsonValues = this.getAllJsonValues(config);
|
|
154
|
+
this.log('JSON values:', JSON.stringify(jsonValues, null, 2));
|
|
155
|
+
if (jsonValues.length > 0) {
|
|
156
|
+
try {
|
|
157
|
+
// De-dup exact schemas
|
|
158
|
+
const schemas = {};
|
|
159
|
+
jsonValues.forEach((json) => {
|
|
160
|
+
const schema = new JsonToZodMapper().resolve(json);
|
|
161
|
+
const stringSchema = new ZodToStringMapper().renderField(schema);
|
|
162
|
+
schemas[stringSchema] = schema;
|
|
163
|
+
});
|
|
164
|
+
const schemaResults = Object.values(schemas);
|
|
165
|
+
// Return a single schema explicitly if we have only one defined
|
|
166
|
+
if (schemaResults.length === 1) {
|
|
167
|
+
return schemaResults[0];
|
|
168
|
+
}
|
|
169
|
+
// Return a union of schemas if we have multiple defined
|
|
170
|
+
return z.union(schemaResults);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.warn(`Error inferring JSON schema for ${key}:`, error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return z.union([z.array(z.any()), z.record(z.string(), z.any())]);
|
|
177
|
+
}
|
|
178
|
+
case 'LOG_LEVEL': {
|
|
179
|
+
return z.enum(['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR']);
|
|
180
|
+
}
|
|
181
|
+
default: {
|
|
182
|
+
return z.any();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
replaceStringsWithMustache(schema, config, schemaLocation = []) {
|
|
187
|
+
const typeName = schema.def.type;
|
|
188
|
+
// Handle enums explicitly
|
|
189
|
+
if (typeName === 'enum') {
|
|
190
|
+
return schema;
|
|
191
|
+
}
|
|
192
|
+
// Check for both direct string and optional string
|
|
193
|
+
if (schema instanceof z.ZodString ||
|
|
194
|
+
(schema instanceof z.ZodOptional && schema.def.innerType instanceof z.ZodString)) {
|
|
195
|
+
const stringsAtLocation = this.getAllStringsAtLocation(config, schemaLocation);
|
|
196
|
+
if (schema instanceof z.ZodOptional) {
|
|
197
|
+
return this.createZodFunctionFromMustacheStrings(stringsAtLocation).optional();
|
|
198
|
+
}
|
|
199
|
+
return this.createZodFunctionFromMustacheStrings(stringsAtLocation);
|
|
200
|
+
}
|
|
201
|
+
if (schema instanceof z.ZodObject) {
|
|
202
|
+
const newShape = {};
|
|
203
|
+
for (const [key, value] of Object.entries(schema.shape)) {
|
|
204
|
+
newShape[key] = this.replaceStringsWithMustache(value, config, [...schemaLocation, key]);
|
|
205
|
+
}
|
|
206
|
+
return z.object(newShape);
|
|
207
|
+
}
|
|
208
|
+
// NOTE: no support for Mustache in Arrays or Unions
|
|
209
|
+
// For all other types, just return as is
|
|
210
|
+
return schema;
|
|
211
|
+
}
|
|
212
|
+
// @ts-expect-error OK with not explicitly returning undefined
|
|
213
|
+
resolveUserSchema(config, configFile) {
|
|
214
|
+
const { schemaKey } = config;
|
|
215
|
+
if (!schemaKey) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const schemaFile = configFile.schemas?.find((schema) => {
|
|
219
|
+
const schemaPath = schema.path.replace(/\\/g, '/');
|
|
220
|
+
const derivedKey = schemaPath.split('/').at(-1)?.replace(/\.json$/, '');
|
|
221
|
+
return derivedKey === schemaKey;
|
|
222
|
+
});
|
|
223
|
+
if (schemaFile) {
|
|
224
|
+
const schema = schemaFile.schema;
|
|
225
|
+
if (isLegacySchemaWrapper(schema)) {
|
|
226
|
+
const legacySchema = schema;
|
|
227
|
+
const defaultSection = legacySchema.default;
|
|
228
|
+
const rules = Array.isArray(defaultSection?.rules) ? defaultSection.rules : [];
|
|
229
|
+
const firstRule = rules[0];
|
|
230
|
+
const value = firstRule?.value;
|
|
231
|
+
const legacySchemaValue = value?.schema;
|
|
232
|
+
const schemaStr = legacySchemaValue?.schema;
|
|
233
|
+
if (typeof schemaStr === 'string') {
|
|
234
|
+
const result = secureEvaluateSchema(schemaStr);
|
|
235
|
+
if (result.success && result.schema) {
|
|
236
|
+
this.log(`Successfully parsed legacy schema wrapper: ${schemaKey}`);
|
|
237
|
+
return result.schema;
|
|
238
|
+
}
|
|
239
|
+
console.warn(`Failed to parse legacy schema wrapper ${schemaKey}: ${result.error}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.log(`Successfully parsed JSON Schema document: ${schemaKey}`);
|
|
244
|
+
return jsonSchemaToZod(schema);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const legacySchemaConfig = configFile.configs.find((c) => c.key === schemaKey && c.configType === 'SCHEMA');
|
|
248
|
+
if (!legacySchemaConfig) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (const row of legacySchemaConfig.rows) {
|
|
252
|
+
for (const valueObj of row.values) {
|
|
253
|
+
if (valueObj.value.schema?.schema) {
|
|
254
|
+
const schemaStr = valueObj.value.schema.schema;
|
|
255
|
+
const result = secureEvaluateSchema(schemaStr);
|
|
256
|
+
if (result.success && result.schema) {
|
|
257
|
+
this.log(`Successfully parsed schema from schema config: ${legacySchemaConfig.key}`);
|
|
258
|
+
return result.schema;
|
|
259
|
+
}
|
|
260
|
+
console.warn(`Failed to parse schema from schema config ${legacySchemaConfig.key}: ${result.error}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { $ZodFunctionArgs, $ZodFunctionOut, util } from 'zod/v4/core';
|
|
3
|
+
export declare enum SupportedLanguage {
|
|
4
|
+
Node = "node-ts",
|
|
5
|
+
React = "react-ts"
|
|
6
|
+
}
|
|
7
|
+
export interface ConfigValue {
|
|
8
|
+
value: {
|
|
9
|
+
bool?: boolean;
|
|
10
|
+
int?: number;
|
|
11
|
+
json?: {
|
|
12
|
+
json: string;
|
|
13
|
+
};
|
|
14
|
+
logLevel?: string;
|
|
15
|
+
schema?: {
|
|
16
|
+
schema: string;
|
|
17
|
+
schemaType: string;
|
|
18
|
+
};
|
|
19
|
+
string?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface SchemaFile {
|
|
23
|
+
path: string;
|
|
24
|
+
schema: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface ConfigRow {
|
|
27
|
+
values: ConfigValue[];
|
|
28
|
+
}
|
|
29
|
+
export interface Config {
|
|
30
|
+
configType: 'CONFIG' | 'FEATURE_FLAG' | 'SCHEMA';
|
|
31
|
+
key: string;
|
|
32
|
+
rows: ConfigRow[];
|
|
33
|
+
schemaKey?: string;
|
|
34
|
+
sendToClientSdk?: boolean;
|
|
35
|
+
valueType: 'BOOL' | 'DOUBLE' | 'DURATION' | 'INT' | 'JSON' | 'LOG_LEVEL' | 'STRING' | 'STRING_LIST';
|
|
36
|
+
}
|
|
37
|
+
export interface ConfigFile {
|
|
38
|
+
configs: Config[];
|
|
39
|
+
schemas?: SchemaFile[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Supported Zod schema types for codegen
|
|
43
|
+
*/
|
|
44
|
+
export type ZodTypeSupported = z.ZodAny | z.ZodArray<z.ZodTypeAny> | z.ZodBoolean | z.ZodDefault<z.ZodTypeAny> | z.ZodEnum<util.EnumLike> | z.ZodFunction<$ZodFunctionArgs, $ZodFunctionOut> | z.ZodNull | z.ZodLiteral<string | number | boolean | null> | z.ZodNumber | z.ZodObject<z.ZodRawShape> | z.ZodOptional<z.ZodTypeAny> | z.ZodRecord | z.ZodNever | z.ZodString | z.ZodTuple | z.ZodUndefined | z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]> | z.ZodUnknown | z.ZodNullable<z.ZodTypeAny> | z.ZodTypeAny;
|