@constela/ai 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/LICENSE +21 -0
- package/dist/index.d.ts +357 -0
- package/dist/index.js +840 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Constela Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
declare const AI_PROVIDERS: readonly ["anthropic", "openai"];
|
|
2
|
+
type AiProviderType = (typeof AI_PROVIDERS)[number];
|
|
3
|
+
declare const AI_OUTPUT_TYPES: readonly ["component", "view", "suggestion"];
|
|
4
|
+
type AiOutputType = (typeof AI_OUTPUT_TYPES)[number];
|
|
5
|
+
interface GenerationContext {
|
|
6
|
+
existingComponents?: string[];
|
|
7
|
+
theme?: Record<string, unknown>;
|
|
8
|
+
schema?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface SecurityOptions$1 {
|
|
11
|
+
allowedTags?: string[];
|
|
12
|
+
allowedActions?: string[];
|
|
13
|
+
allowedUrlPatterns?: string[];
|
|
14
|
+
maxNestingDepth?: number;
|
|
15
|
+
}
|
|
16
|
+
interface GenerateOptions {
|
|
17
|
+
prompt: string;
|
|
18
|
+
output: AiOutputType;
|
|
19
|
+
context?: GenerationContext;
|
|
20
|
+
security?: SecurityOptions$1;
|
|
21
|
+
}
|
|
22
|
+
interface ProviderGenerateOptions {
|
|
23
|
+
model?: string;
|
|
24
|
+
maxTokens?: number;
|
|
25
|
+
temperature?: number;
|
|
26
|
+
systemPrompt?: string;
|
|
27
|
+
}
|
|
28
|
+
interface ProviderResponse {
|
|
29
|
+
content: string;
|
|
30
|
+
model: string;
|
|
31
|
+
usage?: {
|
|
32
|
+
inputTokens: number;
|
|
33
|
+
outputTokens: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
interface AiProvider {
|
|
37
|
+
readonly name: AiProviderType;
|
|
38
|
+
generate(prompt: string, options?: ProviderGenerateOptions): Promise<ProviderResponse>;
|
|
39
|
+
isConfigured(): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type AiErrorCode = 'PROVIDER_NOT_CONFIGURED' | 'PROVIDER_NOT_FOUND' | 'API_ERROR' | 'VALIDATION_ERROR' | 'SECURITY_VIOLATION' | 'RATE_LIMIT_EXCEEDED';
|
|
43
|
+
declare class AiError extends Error {
|
|
44
|
+
readonly code: AiErrorCode;
|
|
45
|
+
constructor(message: string, code: AiErrorCode);
|
|
46
|
+
toJSON(): {
|
|
47
|
+
name: string;
|
|
48
|
+
message: string;
|
|
49
|
+
code: AiErrorCode;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
declare class ValidationError$1 extends AiError {
|
|
53
|
+
readonly violations: string[];
|
|
54
|
+
constructor(message: string, violations: string[]);
|
|
55
|
+
toJSON(): {
|
|
56
|
+
name: string;
|
|
57
|
+
message: string;
|
|
58
|
+
code: AiErrorCode;
|
|
59
|
+
violations: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
declare class SecurityError extends AiError {
|
|
63
|
+
readonly violation: string;
|
|
64
|
+
constructor(message: string, violation: string);
|
|
65
|
+
toJSON(): {
|
|
66
|
+
name: string;
|
|
67
|
+
message: string;
|
|
68
|
+
code: AiErrorCode;
|
|
69
|
+
violation: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Security whitelist definitions for DSL validation
|
|
75
|
+
*
|
|
76
|
+
* Defines forbidden tags and actions that should never appear in AI-generated DSL.
|
|
77
|
+
*/
|
|
78
|
+
declare const FORBIDDEN_TAGS: readonly ["script", "iframe", "object", "embed", "form"];
|
|
79
|
+
declare const FORBIDDEN_ACTIONS: readonly ["import", "call", "dom"];
|
|
80
|
+
declare const RESTRICTED_ACTIONS: readonly ["fetch"];
|
|
81
|
+
type ForbiddenTag = (typeof FORBIDDEN_TAGS)[number];
|
|
82
|
+
type ForbiddenAction = (typeof FORBIDDEN_ACTIONS)[number];
|
|
83
|
+
type RestrictedAction = (typeof RESTRICTED_ACTIONS)[number];
|
|
84
|
+
/**
|
|
85
|
+
* Type guard to check if a tag is forbidden.
|
|
86
|
+
* Case-sensitive: only lowercase tags are forbidden.
|
|
87
|
+
*/
|
|
88
|
+
declare function isForbiddenTag(tag: unknown): tag is ForbiddenTag;
|
|
89
|
+
/**
|
|
90
|
+
* Type guard to check if an action is forbidden.
|
|
91
|
+
* Case-sensitive: only lowercase actions are forbidden.
|
|
92
|
+
*/
|
|
93
|
+
declare function isForbiddenAction(action: unknown): action is ForbiddenAction;
|
|
94
|
+
/**
|
|
95
|
+
* Type guard to check if an action is restricted (requires explicit whitelist).
|
|
96
|
+
* Case-sensitive: only lowercase actions are restricted.
|
|
97
|
+
*/
|
|
98
|
+
declare function isRestrictedAction(action: unknown): action is RestrictedAction;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* URL validation utilities for security enforcement
|
|
102
|
+
*
|
|
103
|
+
* Validates URLs to prevent XSS and other injection attacks.
|
|
104
|
+
*/
|
|
105
|
+
declare const FORBIDDEN_URL_SCHEMES: readonly ["javascript:", "data:", "vbscript:"];
|
|
106
|
+
type ForbiddenUrlScheme = (typeof FORBIDDEN_URL_SCHEMES)[number];
|
|
107
|
+
interface UrlValidationOptions {
|
|
108
|
+
allowedDomains?: string[];
|
|
109
|
+
allowRelative?: boolean;
|
|
110
|
+
}
|
|
111
|
+
interface UrlValidationResult {
|
|
112
|
+
valid: boolean;
|
|
113
|
+
reason?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if a URL uses a forbidden scheme.
|
|
117
|
+
* Case-insensitive for scheme matching.
|
|
118
|
+
*/
|
|
119
|
+
declare function isForbiddenScheme(url: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Validate a URL against security rules.
|
|
122
|
+
*/
|
|
123
|
+
declare function validateUrl(url: string, options?: UrlValidationOptions): UrlValidationResult;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* DSL Validator
|
|
127
|
+
*
|
|
128
|
+
* Validates AI-generated DSL against security rules to prevent XSS,
|
|
129
|
+
* script injection, and other malicious content.
|
|
130
|
+
*/
|
|
131
|
+
/**
|
|
132
|
+
* Security options for validation context
|
|
133
|
+
*/
|
|
134
|
+
interface SecurityOptions {
|
|
135
|
+
allowedTags?: string[];
|
|
136
|
+
allowedActions?: string[];
|
|
137
|
+
allowedUrlPatterns?: string[];
|
|
138
|
+
maxNestingDepth?: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validation context for DSL validation
|
|
142
|
+
*/
|
|
143
|
+
interface ValidationContext {
|
|
144
|
+
security?: SecurityOptions;
|
|
145
|
+
path?: string;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validation error with optional path and code
|
|
149
|
+
*/
|
|
150
|
+
interface ValidationError {
|
|
151
|
+
message: string;
|
|
152
|
+
path?: string;
|
|
153
|
+
code?: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Result of DSL validation
|
|
157
|
+
*/
|
|
158
|
+
interface DslValidationResult {
|
|
159
|
+
valid: boolean;
|
|
160
|
+
errors: ValidationError[];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate a complete DSL structure
|
|
164
|
+
*/
|
|
165
|
+
declare function validateDsl(dsl: unknown, context?: ValidationContext): DslValidationResult;
|
|
166
|
+
/**
|
|
167
|
+
* Validate a single DSL node
|
|
168
|
+
*/
|
|
169
|
+
declare function validateNode(node: unknown, context?: ValidationContext): ValidationError[];
|
|
170
|
+
/**
|
|
171
|
+
* Validate an array of actions
|
|
172
|
+
*/
|
|
173
|
+
declare function validateActions(actions: unknown, context?: ValidationContext): ValidationError[];
|
|
174
|
+
|
|
175
|
+
declare abstract class BaseProvider implements AiProvider {
|
|
176
|
+
abstract readonly name: AiProviderType;
|
|
177
|
+
abstract generate(prompt: string, options?: ProviderGenerateOptions): Promise<ProviderResponse>;
|
|
178
|
+
abstract isConfigured(): boolean;
|
|
179
|
+
protected validatePrompt(prompt: string): void;
|
|
180
|
+
protected getEnvVar(name: string): string | undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface AnthropicProviderOptions {
|
|
184
|
+
apiKey?: string;
|
|
185
|
+
defaultModel?: string;
|
|
186
|
+
}
|
|
187
|
+
declare class AnthropicProvider extends BaseProvider {
|
|
188
|
+
readonly name: AiProviderType;
|
|
189
|
+
private readonly apiKey;
|
|
190
|
+
private readonly defaultModel;
|
|
191
|
+
constructor(options?: AnthropicProviderOptions);
|
|
192
|
+
isConfigured(): boolean;
|
|
193
|
+
generate(prompt: string, options?: ProviderGenerateOptions): Promise<ProviderResponse>;
|
|
194
|
+
}
|
|
195
|
+
declare function createAnthropicProvider(options?: AnthropicProviderOptions): AnthropicProvider;
|
|
196
|
+
|
|
197
|
+
interface OpenAIProviderOptions {
|
|
198
|
+
apiKey?: string;
|
|
199
|
+
defaultModel?: string;
|
|
200
|
+
}
|
|
201
|
+
declare class OpenAIProvider extends BaseProvider {
|
|
202
|
+
readonly name: AiProviderType;
|
|
203
|
+
private readonly apiKey;
|
|
204
|
+
private readonly defaultModel;
|
|
205
|
+
constructor(options?: OpenAIProviderOptions);
|
|
206
|
+
isConfigured(): boolean;
|
|
207
|
+
generate(prompt: string, options?: ProviderGenerateOptions): Promise<ProviderResponse>;
|
|
208
|
+
}
|
|
209
|
+
declare function createOpenAIProvider(options?: OpenAIProviderOptions): OpenAIProvider;
|
|
210
|
+
|
|
211
|
+
interface ProviderFactory {
|
|
212
|
+
create(type: AiProviderType): AiProvider;
|
|
213
|
+
getAvailable(): readonly AiProviderType[];
|
|
214
|
+
isAvailable(type: AiProviderType): boolean;
|
|
215
|
+
}
|
|
216
|
+
declare function createProviderFactory(): ProviderFactory;
|
|
217
|
+
declare function getProvider(type: AiProviderType): AiProvider;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Component prompt builder for Constela DSL generation
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Options for building a component prompt
|
|
225
|
+
*/
|
|
226
|
+
interface ComponentPromptOptions {
|
|
227
|
+
description: string;
|
|
228
|
+
context?: GenerationContext;
|
|
229
|
+
constraints?: string[];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* System prompt for component generation
|
|
233
|
+
*/
|
|
234
|
+
declare const COMPONENT_SYSTEM_PROMPT = "You are a Constela DSL component generator.\n\nYour task is to generate valid Constela DSL JSON for UI components based on user descriptions.\n\nOutput Requirements:\n- Return valid JSON representing a Constela DSL component\n- The JSON must have a \"type\" property specifying the component type\n- Include a \"props\" object for component properties\n- Include \"children\" array for nested elements if needed\n- Include \"actions\" array for event handlers if needed\n\nSecurity Rules:\n- Do NOT use forbidden tags: script, iframe, object, embed, form\n- Do NOT use forbidden actions: import, call, dom\n- Keep nesting depth reasonable\n\nAlways output raw JSON, optionally wrapped in a markdown code block.";
|
|
235
|
+
/**
|
|
236
|
+
* Build a user prompt for component generation
|
|
237
|
+
*/
|
|
238
|
+
declare function buildComponentPrompt(options: ComponentPromptOptions): string;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* View prompt builder for Constela DSL generation
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Options for building a view prompt
|
|
246
|
+
*/
|
|
247
|
+
interface ViewPromptOptions {
|
|
248
|
+
description: string;
|
|
249
|
+
context?: GenerationContext;
|
|
250
|
+
constraints?: string[];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* System prompt for view generation
|
|
254
|
+
*/
|
|
255
|
+
declare const VIEW_SYSTEM_PROMPT = "You are a Constela DSL view generator.\n\nYour task is to generate valid Constela DSL JSON for complete views, pages, screens, and layouts based on user descriptions.\n\nOutput Requirements:\n- Return valid JSON representing a Constela DSL view\n- The root element should typically be a \"view\" or layout component\n- Include a \"props\" object for view properties (title, layout options, etc.)\n- Include \"children\" array for the view's content structure\n- Include \"actions\" array for page-level event handlers if needed\n- Structure the view with proper layout containers and nested components\n\nSecurity Rules:\n- Do NOT use forbidden tags: script, iframe, object, embed, form\n- Do NOT use forbidden actions: import, call, dom\n- Keep nesting depth reasonable\n\nAlways output raw JSON, optionally wrapped in a markdown code block.";
|
|
256
|
+
/**
|
|
257
|
+
* Build a user prompt for view generation
|
|
258
|
+
*/
|
|
259
|
+
declare function buildViewPrompt(options: ViewPromptOptions): string;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Suggest prompt builder for Constela DSL analysis
|
|
263
|
+
*/
|
|
264
|
+
/**
|
|
265
|
+
* Aspects that can be analyzed for suggestions
|
|
266
|
+
*/
|
|
267
|
+
type SuggestionAspect = 'accessibility' | 'performance' | 'security' | 'ux';
|
|
268
|
+
/**
|
|
269
|
+
* Severity levels for suggestions
|
|
270
|
+
*/
|
|
271
|
+
type SuggestionSeverity = 'low' | 'medium' | 'high';
|
|
272
|
+
/**
|
|
273
|
+
* A suggestion for improving DSL
|
|
274
|
+
*/
|
|
275
|
+
interface Suggestion {
|
|
276
|
+
aspect: SuggestionAspect;
|
|
277
|
+
issue: string;
|
|
278
|
+
recommendation: string;
|
|
279
|
+
location?: string;
|
|
280
|
+
severity: SuggestionSeverity;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Options for building a suggest prompt
|
|
284
|
+
*/
|
|
285
|
+
interface SuggestPromptOptions {
|
|
286
|
+
dsl: unknown;
|
|
287
|
+
aspect: SuggestionAspect;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* System prompt for suggestion generation
|
|
291
|
+
*/
|
|
292
|
+
declare const SUGGEST_SYSTEM_PROMPT = "You are a Constela DSL code reviewer.\n\nYour task is to analyze Constela DSL and provide suggestions for improvements.\n\nOutput Requirements:\n- Return a JSON array of suggestion objects\n- Each suggestion must have: aspect, issue, recommendation, severity\n- Optionally include location (e.g., \"children[0].props\")\n- Severity levels: low, medium, high\n- Aspects: accessibility, performance, security, ux\n\nFocus Areas by Aspect:\n- accessibility: ARIA labels, keyboard navigation, screen reader support, color contrast\n- performance: unnecessary nesting, heavy components, render optimization\n- security: dangerous props, untrusted URLs, injection risks\n- ux: usability issues, confusing layouts, missing feedback, touch targets\n\nAlways output a JSON array, optionally wrapped in a markdown code block.";
|
|
293
|
+
/**
|
|
294
|
+
* Build a user prompt for suggestion generation
|
|
295
|
+
*/
|
|
296
|
+
declare function buildSuggestPrompt(options: SuggestPromptOptions): string;
|
|
297
|
+
/**
|
|
298
|
+
* Parse AI response into suggestions array
|
|
299
|
+
* Returns empty array for invalid responses
|
|
300
|
+
*/
|
|
301
|
+
declare function parseSuggestions(response: string): Suggestion[];
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* DSL Generator
|
|
305
|
+
*
|
|
306
|
+
* Main class for generating Constela DSL using AI providers
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Options for creating a DslGenerator
|
|
311
|
+
*/
|
|
312
|
+
interface DslGeneratorOptions {
|
|
313
|
+
provider: AiProviderType;
|
|
314
|
+
providerInstance?: AiProvider;
|
|
315
|
+
security?: SecurityOptions$1;
|
|
316
|
+
context?: GenerationContext;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Result of DSL generation
|
|
320
|
+
*/
|
|
321
|
+
interface GenerateResult {
|
|
322
|
+
dsl: Record<string, unknown>;
|
|
323
|
+
raw: string;
|
|
324
|
+
validated: boolean;
|
|
325
|
+
errors?: string[];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Main class for generating DSL
|
|
329
|
+
*/
|
|
330
|
+
declare class DslGenerator {
|
|
331
|
+
private readonly providerType;
|
|
332
|
+
private readonly provider;
|
|
333
|
+
private readonly security;
|
|
334
|
+
private readonly context;
|
|
335
|
+
constructor(options: DslGeneratorOptions);
|
|
336
|
+
/**
|
|
337
|
+
* Generate DSL from a prompt
|
|
338
|
+
*/
|
|
339
|
+
generate(options: GenerateOptions): Promise<GenerateResult>;
|
|
340
|
+
/**
|
|
341
|
+
* Validate DSL against security rules
|
|
342
|
+
*/
|
|
343
|
+
validate(dsl: unknown, securityOverrides?: SecurityOptions$1): {
|
|
344
|
+
valid: boolean;
|
|
345
|
+
errors: string[];
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Parse AI response and extract JSON
|
|
349
|
+
*/
|
|
350
|
+
parseResponse(response: string): Record<string, unknown>;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Factory function to create a DslGenerator
|
|
354
|
+
*/
|
|
355
|
+
declare function createDslGenerator(options: DslGeneratorOptions): DslGenerator;
|
|
356
|
+
|
|
357
|
+
export { AI_OUTPUT_TYPES, AI_PROVIDERS, AiError, type AiErrorCode, type AiOutputType, type AiProvider, type AiProviderType, AnthropicProvider, type AnthropicProviderOptions, BaseProvider, COMPONENT_SYSTEM_PROMPT, type ComponentPromptOptions, DslGenerator, type DslGeneratorOptions, type ValidationError as DslValidationError, type DslValidationResult, FORBIDDEN_ACTIONS, FORBIDDEN_TAGS, FORBIDDEN_URL_SCHEMES, type ForbiddenAction, type ForbiddenTag, type ForbiddenUrlScheme, type GenerateOptions, type GenerateResult, type GenerationContext, OpenAIProvider, type OpenAIProviderOptions, type ProviderFactory, type ProviderGenerateOptions, type ProviderResponse, RESTRICTED_ACTIONS, type RestrictedAction, SUGGEST_SYSTEM_PROMPT, SecurityError, type SecurityOptions$1 as SecurityOptions, type SuggestPromptOptions, type Suggestion, type SuggestionAspect, type UrlValidationOptions, type UrlValidationResult, VIEW_SYSTEM_PROMPT, type ValidationContext, ValidationError$1 as ValidationError, type ViewPromptOptions, buildComponentPrompt, buildSuggestPrompt, buildViewPrompt, createAnthropicProvider, createDslGenerator, createOpenAIProvider, createProviderFactory, getProvider, isForbiddenAction, isForbiddenScheme, isForbiddenTag, isRestrictedAction, parseSuggestions, validateActions, validateDsl, validateNode, validateUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var AI_PROVIDERS = Object.freeze(["anthropic", "openai"]);
|
|
3
|
+
var AI_OUTPUT_TYPES = Object.freeze(["component", "view", "suggestion"]);
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var AiError = class _AiError extends Error {
|
|
7
|
+
constructor(message, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "AiError";
|
|
11
|
+
Object.setPrototypeOf(this, _AiError.prototype);
|
|
12
|
+
}
|
|
13
|
+
toJSON() {
|
|
14
|
+
return {
|
|
15
|
+
name: this.name,
|
|
16
|
+
message: this.message,
|
|
17
|
+
code: this.code
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var ValidationError = class _ValidationError extends AiError {
|
|
22
|
+
constructor(message, violations) {
|
|
23
|
+
super(message, "VALIDATION_ERROR");
|
|
24
|
+
this.violations = violations;
|
|
25
|
+
this.name = "ValidationError";
|
|
26
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
27
|
+
}
|
|
28
|
+
toJSON() {
|
|
29
|
+
return {
|
|
30
|
+
name: this.name,
|
|
31
|
+
message: this.message,
|
|
32
|
+
code: this.code,
|
|
33
|
+
violations: this.violations
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var SecurityError = class _SecurityError extends AiError {
|
|
38
|
+
constructor(message, violation) {
|
|
39
|
+
super(message, "SECURITY_VIOLATION");
|
|
40
|
+
this.violation = violation;
|
|
41
|
+
this.name = "SecurityError";
|
|
42
|
+
Object.setPrototypeOf(this, _SecurityError.prototype);
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
name: this.name,
|
|
47
|
+
message: this.message,
|
|
48
|
+
code: this.code,
|
|
49
|
+
violation: this.violation
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/security/whitelist.ts
|
|
55
|
+
var FORBIDDEN_TAGS = Object.freeze([
|
|
56
|
+
"script",
|
|
57
|
+
"iframe",
|
|
58
|
+
"object",
|
|
59
|
+
"embed",
|
|
60
|
+
"form"
|
|
61
|
+
]);
|
|
62
|
+
var FORBIDDEN_ACTIONS = Object.freeze([
|
|
63
|
+
"import",
|
|
64
|
+
"call",
|
|
65
|
+
"dom"
|
|
66
|
+
]);
|
|
67
|
+
var RESTRICTED_ACTIONS = Object.freeze([
|
|
68
|
+
"fetch"
|
|
69
|
+
]);
|
|
70
|
+
function isForbiddenTag(tag) {
|
|
71
|
+
if (typeof tag !== "string") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return FORBIDDEN_TAGS.includes(tag);
|
|
75
|
+
}
|
|
76
|
+
function isForbiddenAction(action) {
|
|
77
|
+
if (typeof action !== "string") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return FORBIDDEN_ACTIONS.includes(action);
|
|
81
|
+
}
|
|
82
|
+
function isRestrictedAction(action) {
|
|
83
|
+
if (typeof action !== "string") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return RESTRICTED_ACTIONS.includes(action);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/security/url-validator.ts
|
|
90
|
+
var FORBIDDEN_URL_SCHEMES = Object.freeze([
|
|
91
|
+
"javascript:",
|
|
92
|
+
"data:",
|
|
93
|
+
"vbscript:"
|
|
94
|
+
]);
|
|
95
|
+
function isForbiddenScheme(url) {
|
|
96
|
+
const trimmedUrl = url.trim().toLowerCase();
|
|
97
|
+
const decodedUrl = tryDecodeUri(trimmedUrl);
|
|
98
|
+
return FORBIDDEN_URL_SCHEMES.some(
|
|
99
|
+
(scheme) => trimmedUrl.startsWith(scheme) || decodedUrl.startsWith(scheme)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function tryDecodeUri(url) {
|
|
103
|
+
try {
|
|
104
|
+
return decodeURIComponent(url);
|
|
105
|
+
} catch {
|
|
106
|
+
return url;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateUrl(url, options) {
|
|
110
|
+
const trimmedUrl = url.trim();
|
|
111
|
+
if (trimmedUrl === "") {
|
|
112
|
+
return { valid: false, reason: "Empty URL is not allowed" };
|
|
113
|
+
}
|
|
114
|
+
if (isForbiddenScheme(trimmedUrl)) {
|
|
115
|
+
const lowerUrl = trimmedUrl.toLowerCase();
|
|
116
|
+
let scheme = "unknown";
|
|
117
|
+
for (const forbiddenScheme of FORBIDDEN_URL_SCHEMES) {
|
|
118
|
+
if (lowerUrl.startsWith(forbiddenScheme) || tryDecodeUri(lowerUrl).startsWith(forbiddenScheme)) {
|
|
119
|
+
scheme = forbiddenScheme.replace(":", "");
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { valid: false, reason: `Forbidden URL scheme: ${scheme}` };
|
|
124
|
+
}
|
|
125
|
+
const isRelative = !trimmedUrl.includes("://") && !trimmedUrl.startsWith("//");
|
|
126
|
+
const allowRelative = options?.allowRelative ?? true;
|
|
127
|
+
if (isRelative) {
|
|
128
|
+
return allowRelative ? { valid: true } : { valid: false, reason: "Relative URLs are not allowed" };
|
|
129
|
+
}
|
|
130
|
+
if (options?.allowedDomains && options.allowedDomains.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
const urlObj = new URL(trimmedUrl);
|
|
133
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
134
|
+
const isAllowed = options.allowedDomains.some((domain) => {
|
|
135
|
+
const lowerDomain = domain.toLowerCase();
|
|
136
|
+
return hostname === lowerDomain;
|
|
137
|
+
});
|
|
138
|
+
if (!isAllowed) {
|
|
139
|
+
return { valid: false, reason: `Domain not in allowed list: ${hostname}` };
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
return { valid: false, reason: "Invalid URL format" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { valid: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/validator.ts
|
|
149
|
+
var DEFAULT_MAX_NESTING_DEPTH = 32;
|
|
150
|
+
function validateDsl(dsl, context) {
|
|
151
|
+
const errors = [];
|
|
152
|
+
const basePath = context?.path ?? "root";
|
|
153
|
+
if (!isPlainObject(dsl)) {
|
|
154
|
+
errors.push({
|
|
155
|
+
message: "DSL must be a non-null object",
|
|
156
|
+
path: basePath,
|
|
157
|
+
code: "INVALID_DSL"
|
|
158
|
+
});
|
|
159
|
+
return { valid: false, errors };
|
|
160
|
+
}
|
|
161
|
+
const nodeErrors = validateNodeRecursive(
|
|
162
|
+
dsl,
|
|
163
|
+
{ ...context, path: basePath },
|
|
164
|
+
0
|
|
165
|
+
);
|
|
166
|
+
errors.push(...nodeErrors);
|
|
167
|
+
return {
|
|
168
|
+
valid: errors.length === 0,
|
|
169
|
+
errors
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function validateNode(node, context) {
|
|
173
|
+
if (!isPlainObject(node)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const errors = [];
|
|
177
|
+
const path = context?.path ?? "node";
|
|
178
|
+
const dslNode = node;
|
|
179
|
+
if (dslNode.type && isForbiddenTag(dslNode.type)) {
|
|
180
|
+
errors.push({
|
|
181
|
+
message: "Forbidden tag: " + dslNode.type,
|
|
182
|
+
path,
|
|
183
|
+
code: "FORBIDDEN_TAG"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (context?.security?.allowedTags && context.security.allowedTags.length > 0 && dslNode.type) {
|
|
187
|
+
if (!context.security.allowedTags.includes(dslNode.type)) {
|
|
188
|
+
errors.push({
|
|
189
|
+
message: "Tag not in allowed list: " + dslNode.type,
|
|
190
|
+
path,
|
|
191
|
+
code: "TAG_NOT_ALLOWED"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (dslNode.actions) {
|
|
196
|
+
const actionErrors = validateActions(dslNode.actions, {
|
|
197
|
+
...context,
|
|
198
|
+
path: path + ".actions"
|
|
199
|
+
});
|
|
200
|
+
errors.push(...actionErrors);
|
|
201
|
+
}
|
|
202
|
+
return errors;
|
|
203
|
+
}
|
|
204
|
+
function validateActions(actions, context) {
|
|
205
|
+
if (actions === null || actions === void 0) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(actions)) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const errors = [];
|
|
212
|
+
const basePath = context?.path ?? "actions";
|
|
213
|
+
const allowedActions = context?.security?.allowedActions ?? [];
|
|
214
|
+
const allowedUrlPatterns = context?.security?.allowedUrlPatterns ?? [];
|
|
215
|
+
actions.forEach((action, index) => {
|
|
216
|
+
if (!isPlainObject(action)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const actionPath = basePath + "[" + index + "]";
|
|
220
|
+
const dslAction = action;
|
|
221
|
+
const actionType = dslAction.type;
|
|
222
|
+
if (!actionType) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (isForbiddenAction(actionType)) {
|
|
226
|
+
errors.push({
|
|
227
|
+
message: "Forbidden action: " + actionType,
|
|
228
|
+
path: actionPath,
|
|
229
|
+
code: "FORBIDDEN_ACTION"
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (isRestrictedAction(actionType)) {
|
|
234
|
+
if (!allowedActions.includes(actionType)) {
|
|
235
|
+
errors.push({
|
|
236
|
+
message: "Restricted action requires explicit whitelist: " + actionType,
|
|
237
|
+
path: actionPath,
|
|
238
|
+
code: "RESTRICTED_ACTION"
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (actionType === "fetch" && dslAction.payload?.["url"]) {
|
|
243
|
+
const url = String(dslAction.payload["url"]);
|
|
244
|
+
const urlValidation = validateActionUrl(url, allowedUrlPatterns);
|
|
245
|
+
if (!urlValidation.valid) {
|
|
246
|
+
errors.push({
|
|
247
|
+
message: urlValidation.reason ?? "Invalid URL",
|
|
248
|
+
path: actionPath + ".payload.url",
|
|
249
|
+
code: "INVALID_URL"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (context?.security?.allowedActions && !isStandardAction(actionType) && !allowedActions.includes(actionType)) {
|
|
255
|
+
errors.push({
|
|
256
|
+
message: "Action not in allowed list: " + actionType,
|
|
257
|
+
path: actionPath,
|
|
258
|
+
code: "ACTION_NOT_ALLOWED"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (actionType === "navigate" && dslAction.payload) {
|
|
262
|
+
const url = dslAction.payload["url"] ?? dslAction.payload["path"];
|
|
263
|
+
if (url && typeof url === "string") {
|
|
264
|
+
const urlResult = validateUrl(url);
|
|
265
|
+
if (!urlResult.valid) {
|
|
266
|
+
errors.push({
|
|
267
|
+
message: urlResult.reason ?? "Invalid navigation URL",
|
|
268
|
+
path: actionPath + ".payload.url",
|
|
269
|
+
code: "INVALID_URL"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return errors;
|
|
276
|
+
}
|
|
277
|
+
var STANDARD_ACTIONS = ["navigate", "setState", "emit", "submit"];
|
|
278
|
+
function isStandardAction(action) {
|
|
279
|
+
return STANDARD_ACTIONS.includes(action);
|
|
280
|
+
}
|
|
281
|
+
function validateNodeRecursive(node, context, depth) {
|
|
282
|
+
const errors = [];
|
|
283
|
+
const path = context.path ?? "root";
|
|
284
|
+
const maxDepth = context.security?.maxNestingDepth ?? DEFAULT_MAX_NESTING_DEPTH;
|
|
285
|
+
if (depth > maxDepth) {
|
|
286
|
+
errors.push({
|
|
287
|
+
message: "Maximum nesting depth exceeded: " + depth + " > " + maxDepth,
|
|
288
|
+
path,
|
|
289
|
+
code: "MAX_DEPTH_EXCEEDED"
|
|
290
|
+
});
|
|
291
|
+
return errors;
|
|
292
|
+
}
|
|
293
|
+
const nodeErrors = validateNode(node, context);
|
|
294
|
+
errors.push(...nodeErrors);
|
|
295
|
+
if (Array.isArray(node.children)) {
|
|
296
|
+
node.children.forEach((child, index) => {
|
|
297
|
+
if (isPlainObject(child)) {
|
|
298
|
+
const childErrors = validateNodeRecursive(
|
|
299
|
+
child,
|
|
300
|
+
{ ...context, path: path + ".children[" + index + "]" },
|
|
301
|
+
depth + 1
|
|
302
|
+
);
|
|
303
|
+
errors.push(...childErrors);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return errors;
|
|
308
|
+
}
|
|
309
|
+
function validateActionUrl(url, allowedPatterns) {
|
|
310
|
+
if (isForbiddenScheme(url)) {
|
|
311
|
+
const scheme = url.toLowerCase().split(":")[0];
|
|
312
|
+
return { valid: false, reason: "Forbidden URL scheme: " + scheme };
|
|
313
|
+
}
|
|
314
|
+
if (allowedPatterns.length === 0) {
|
|
315
|
+
return { valid: true };
|
|
316
|
+
}
|
|
317
|
+
for (const pattern of allowedPatterns) {
|
|
318
|
+
if (matchUrlPattern(url, pattern)) {
|
|
319
|
+
return { valid: true };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { valid: false, reason: "URL does not match allowed patterns: " + url };
|
|
323
|
+
}
|
|
324
|
+
function matchUrlPattern(url, pattern) {
|
|
325
|
+
let regexPattern = "";
|
|
326
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
327
|
+
const char = pattern[i];
|
|
328
|
+
if (char === "*") {
|
|
329
|
+
regexPattern += ".*";
|
|
330
|
+
} else if (".+?^${}()|[]\\".includes(char)) {
|
|
331
|
+
regexPattern += "\\" + char;
|
|
332
|
+
} else {
|
|
333
|
+
regexPattern += char;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const regex = new RegExp("^" + regexPattern + "$");
|
|
337
|
+
return regex.test(url);
|
|
338
|
+
}
|
|
339
|
+
function isPlainObject(value) {
|
|
340
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/providers/anthropic.ts
|
|
344
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
345
|
+
|
|
346
|
+
// src/providers/base.ts
|
|
347
|
+
var BaseProvider = class {
|
|
348
|
+
validatePrompt(prompt) {
|
|
349
|
+
if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
|
|
350
|
+
throw new AiError("Prompt cannot be empty", "VALIDATION_ERROR");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
getEnvVar(name) {
|
|
354
|
+
return process.env[name];
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// src/providers/anthropic.ts
|
|
359
|
+
var DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
360
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
361
|
+
var AnthropicProvider = class extends BaseProvider {
|
|
362
|
+
name = "anthropic";
|
|
363
|
+
apiKey;
|
|
364
|
+
defaultModel;
|
|
365
|
+
constructor(options) {
|
|
366
|
+
super();
|
|
367
|
+
this.apiKey = options?.apiKey ?? this.getEnvVar("ANTHROPIC_API_KEY");
|
|
368
|
+
this.defaultModel = options?.defaultModel ?? DEFAULT_MODEL;
|
|
369
|
+
}
|
|
370
|
+
isConfigured() {
|
|
371
|
+
return Boolean(this.apiKey && this.apiKey.length > 0);
|
|
372
|
+
}
|
|
373
|
+
async generate(prompt, options) {
|
|
374
|
+
if (!this.isConfigured()) {
|
|
375
|
+
throw new AiError(
|
|
376
|
+
"Anthropic provider is not configured. Please set ANTHROPIC_API_KEY environment variable or provide apiKey in options.",
|
|
377
|
+
"PROVIDER_NOT_CONFIGURED"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
this.validatePrompt(prompt);
|
|
381
|
+
try {
|
|
382
|
+
const client = new Anthropic({ apiKey: this.apiKey });
|
|
383
|
+
const params = {
|
|
384
|
+
model: options?.model ?? this.defaultModel,
|
|
385
|
+
max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
386
|
+
messages: [{ role: "user", content: prompt }]
|
|
387
|
+
};
|
|
388
|
+
if (options?.temperature !== void 0) {
|
|
389
|
+
params.temperature = options.temperature;
|
|
390
|
+
}
|
|
391
|
+
if (options?.systemPrompt !== void 0) {
|
|
392
|
+
params.system = options.systemPrompt;
|
|
393
|
+
}
|
|
394
|
+
const response = await client.messages.create(params);
|
|
395
|
+
const textContent = response.content.find((block) => block.type === "text");
|
|
396
|
+
const content = textContent && "text" in textContent ? textContent.text : "";
|
|
397
|
+
const result = {
|
|
398
|
+
content,
|
|
399
|
+
model: response.model
|
|
400
|
+
};
|
|
401
|
+
if (response.usage) {
|
|
402
|
+
result.usage = {
|
|
403
|
+
inputTokens: response.usage.input_tokens,
|
|
404
|
+
outputTokens: response.usage.output_tokens
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error instanceof AiError) {
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
413
|
+
throw new AiError(`Anthropic API error: ${message}`, "API_ERROR");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function createAnthropicProvider(options) {
|
|
418
|
+
return new AnthropicProvider(options);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/providers/openai.ts
|
|
422
|
+
import OpenAI from "openai";
|
|
423
|
+
var DEFAULT_MODEL2 = "gpt-4o";
|
|
424
|
+
var DEFAULT_MAX_TOKENS2 = 4096;
|
|
425
|
+
var OpenAIProvider = class extends BaseProvider {
|
|
426
|
+
name = "openai";
|
|
427
|
+
apiKey;
|
|
428
|
+
defaultModel;
|
|
429
|
+
constructor(options) {
|
|
430
|
+
super();
|
|
431
|
+
this.apiKey = options?.apiKey ?? this.getEnvVar("OPENAI_API_KEY");
|
|
432
|
+
this.defaultModel = options?.defaultModel ?? DEFAULT_MODEL2;
|
|
433
|
+
}
|
|
434
|
+
isConfigured() {
|
|
435
|
+
return Boolean(this.apiKey && this.apiKey.length > 0);
|
|
436
|
+
}
|
|
437
|
+
async generate(prompt, options) {
|
|
438
|
+
if (!this.isConfigured()) {
|
|
439
|
+
throw new AiError(
|
|
440
|
+
"OpenAI provider is not configured. Please set OPENAI_API_KEY environment variable or provide apiKey in options.",
|
|
441
|
+
"PROVIDER_NOT_CONFIGURED"
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
this.validatePrompt(prompt);
|
|
445
|
+
try {
|
|
446
|
+
const client = new OpenAI({ apiKey: this.apiKey });
|
|
447
|
+
const messages = [];
|
|
448
|
+
if (options?.systemPrompt) {
|
|
449
|
+
messages.push({ role: "system", content: options.systemPrompt });
|
|
450
|
+
}
|
|
451
|
+
messages.push({ role: "user", content: prompt });
|
|
452
|
+
const params = {
|
|
453
|
+
model: options?.model ?? this.defaultModel,
|
|
454
|
+
max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
455
|
+
messages
|
|
456
|
+
};
|
|
457
|
+
if (options?.temperature !== void 0) {
|
|
458
|
+
params.temperature = options.temperature;
|
|
459
|
+
}
|
|
460
|
+
const response = await client.chat.completions.create(params);
|
|
461
|
+
const content = response.choices[0]?.message?.content;
|
|
462
|
+
if (content === null || content === void 0) {
|
|
463
|
+
throw new AiError("OpenAI returned empty response", "API_ERROR");
|
|
464
|
+
}
|
|
465
|
+
const result = {
|
|
466
|
+
content,
|
|
467
|
+
model: response.model
|
|
468
|
+
};
|
|
469
|
+
if (response.usage) {
|
|
470
|
+
result.usage = {
|
|
471
|
+
inputTokens: response.usage.prompt_tokens,
|
|
472
|
+
outputTokens: response.usage.completion_tokens
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof AiError) {
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
481
|
+
throw new AiError(`OpenAI API error: ${message}`, "API_ERROR");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
function createOpenAIProvider(options) {
|
|
486
|
+
return new OpenAIProvider(options);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/providers/index.ts
|
|
490
|
+
function createProviderFactory() {
|
|
491
|
+
return {
|
|
492
|
+
create(type) {
|
|
493
|
+
switch (type) {
|
|
494
|
+
case "anthropic":
|
|
495
|
+
return createAnthropicProvider();
|
|
496
|
+
case "openai":
|
|
497
|
+
return createOpenAIProvider();
|
|
498
|
+
default:
|
|
499
|
+
throw new AiError(
|
|
500
|
+
`Unknown provider type: ${type}. Available providers: ${AI_PROVIDERS.join(", ")}`,
|
|
501
|
+
"PROVIDER_NOT_FOUND"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
getAvailable() {
|
|
506
|
+
return AI_PROVIDERS;
|
|
507
|
+
},
|
|
508
|
+
isAvailable(type) {
|
|
509
|
+
try {
|
|
510
|
+
const provider = this.create(type);
|
|
511
|
+
return provider.isConfigured();
|
|
512
|
+
} catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function getProvider(type) {
|
|
519
|
+
const factory = createProviderFactory();
|
|
520
|
+
return factory.create(type);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/prompts/component.ts
|
|
524
|
+
var COMPONENT_SYSTEM_PROMPT = `You are a Constela DSL component generator.
|
|
525
|
+
|
|
526
|
+
Your task is to generate valid Constela DSL JSON for UI components based on user descriptions.
|
|
527
|
+
|
|
528
|
+
Output Requirements:
|
|
529
|
+
- Return valid JSON representing a Constela DSL component
|
|
530
|
+
- The JSON must have a "type" property specifying the component type
|
|
531
|
+
- Include a "props" object for component properties
|
|
532
|
+
- Include "children" array for nested elements if needed
|
|
533
|
+
- Include "actions" array for event handlers if needed
|
|
534
|
+
|
|
535
|
+
Security Rules:
|
|
536
|
+
- Do NOT use forbidden tags: script, iframe, object, embed, form
|
|
537
|
+
- Do NOT use forbidden actions: import, call, dom
|
|
538
|
+
- Keep nesting depth reasonable
|
|
539
|
+
|
|
540
|
+
Always output raw JSON, optionally wrapped in a markdown code block.`;
|
|
541
|
+
function buildComponentPrompt(options) {
|
|
542
|
+
const { description, context, constraints } = options;
|
|
543
|
+
const parts = [];
|
|
544
|
+
parts.push("Create a Constela DSL component based on the following description:\n\n" + description);
|
|
545
|
+
if (context) {
|
|
546
|
+
const contextParts = [];
|
|
547
|
+
if (context.existingComponents && context.existingComponents.length > 0) {
|
|
548
|
+
contextParts.push("Available components: " + context.existingComponents.join(", "));
|
|
549
|
+
}
|
|
550
|
+
if (context.theme && Object.keys(context.theme).length > 0) {
|
|
551
|
+
contextParts.push("Theme: " + JSON.stringify(context.theme));
|
|
552
|
+
}
|
|
553
|
+
if (context.schema && Object.keys(context.schema).length > 0) {
|
|
554
|
+
contextParts.push("Schema: " + JSON.stringify(context.schema));
|
|
555
|
+
}
|
|
556
|
+
if (contextParts.length > 0) {
|
|
557
|
+
parts.push("\nContext:\n" + contextParts.join("\n"));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (constraints && constraints.length > 0) {
|
|
561
|
+
parts.push("\nConstraints:\n" + constraints.map((c) => "- " + c).join("\n"));
|
|
562
|
+
}
|
|
563
|
+
return parts.join("\n");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/prompts/view.ts
|
|
567
|
+
var VIEW_SYSTEM_PROMPT = `You are a Constela DSL view generator.
|
|
568
|
+
|
|
569
|
+
Your task is to generate valid Constela DSL JSON for complete views, pages, screens, and layouts based on user descriptions.
|
|
570
|
+
|
|
571
|
+
Output Requirements:
|
|
572
|
+
- Return valid JSON representing a Constela DSL view
|
|
573
|
+
- The root element should typically be a "view" or layout component
|
|
574
|
+
- Include a "props" object for view properties (title, layout options, etc.)
|
|
575
|
+
- Include "children" array for the view's content structure
|
|
576
|
+
- Include "actions" array for page-level event handlers if needed
|
|
577
|
+
- Structure the view with proper layout containers and nested components
|
|
578
|
+
|
|
579
|
+
Security Rules:
|
|
580
|
+
- Do NOT use forbidden tags: script, iframe, object, embed, form
|
|
581
|
+
- Do NOT use forbidden actions: import, call, dom
|
|
582
|
+
- Keep nesting depth reasonable
|
|
583
|
+
|
|
584
|
+
Always output raw JSON, optionally wrapped in a markdown code block.`;
|
|
585
|
+
function buildViewPrompt(options) {
|
|
586
|
+
const { description, context, constraints } = options;
|
|
587
|
+
const parts = [];
|
|
588
|
+
parts.push("Create a Constela DSL view based on the following description:\n\n" + description);
|
|
589
|
+
if (context) {
|
|
590
|
+
const contextParts = [];
|
|
591
|
+
if (context.existingComponents && context.existingComponents.length > 0) {
|
|
592
|
+
contextParts.push("Available components: " + context.existingComponents.join(", "));
|
|
593
|
+
}
|
|
594
|
+
if (context.theme && Object.keys(context.theme).length > 0) {
|
|
595
|
+
contextParts.push("Theme: " + JSON.stringify(context.theme));
|
|
596
|
+
}
|
|
597
|
+
if (context.schema && Object.keys(context.schema).length > 0) {
|
|
598
|
+
contextParts.push("Schema: " + JSON.stringify(context.schema));
|
|
599
|
+
}
|
|
600
|
+
if (contextParts.length > 0) {
|
|
601
|
+
parts.push("\nContext:\n" + contextParts.join("\n"));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (constraints && constraints.length > 0) {
|
|
605
|
+
parts.push("\nConstraints:\n" + constraints.map((c) => "- " + c).join("\n"));
|
|
606
|
+
}
|
|
607
|
+
return parts.join("\n");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/prompts/suggest.ts
|
|
611
|
+
var VALID_ASPECTS = ["accessibility", "performance", "security", "ux"];
|
|
612
|
+
var VALID_SEVERITIES = ["low", "medium", "high"];
|
|
613
|
+
var SUGGEST_SYSTEM_PROMPT = `You are a Constela DSL code reviewer.
|
|
614
|
+
|
|
615
|
+
Your task is to analyze Constela DSL and provide suggestions for improvements.
|
|
616
|
+
|
|
617
|
+
Output Requirements:
|
|
618
|
+
- Return a JSON array of suggestion objects
|
|
619
|
+
- Each suggestion must have: aspect, issue, recommendation, severity
|
|
620
|
+
- Optionally include location (e.g., "children[0].props")
|
|
621
|
+
- Severity levels: low, medium, high
|
|
622
|
+
- Aspects: accessibility, performance, security, ux
|
|
623
|
+
|
|
624
|
+
Focus Areas by Aspect:
|
|
625
|
+
- accessibility: ARIA labels, keyboard navigation, screen reader support, color contrast
|
|
626
|
+
- performance: unnecessary nesting, heavy components, render optimization
|
|
627
|
+
- security: dangerous props, untrusted URLs, injection risks
|
|
628
|
+
- ux: usability issues, confusing layouts, missing feedback, touch targets
|
|
629
|
+
|
|
630
|
+
Always output a JSON array, optionally wrapped in a markdown code block.`;
|
|
631
|
+
function buildSuggestPrompt(options) {
|
|
632
|
+
const { dsl, aspect } = options;
|
|
633
|
+
const dslString = typeof dsl === "string" ? dsl : JSON.stringify(dsl, null, 2);
|
|
634
|
+
return `Analyze the following Constela DSL for ${aspect} issues and provide suggestions:
|
|
635
|
+
|
|
636
|
+
\`\`\`json
|
|
637
|
+
${dslString}
|
|
638
|
+
\`\`\`
|
|
639
|
+
|
|
640
|
+
Focus specifically on ${aspect} concerns. Return a JSON array of suggestions.`;
|
|
641
|
+
}
|
|
642
|
+
function extractJson(response) {
|
|
643
|
+
const trimmed = response.trim();
|
|
644
|
+
if (trimmed === "") {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
648
|
+
if (codeBlockMatch?.[1]) {
|
|
649
|
+
return codeBlockMatch[1].trim();
|
|
650
|
+
}
|
|
651
|
+
return trimmed;
|
|
652
|
+
}
|
|
653
|
+
function isValidSuggestion(obj) {
|
|
654
|
+
if (typeof obj !== "object" || obj === null) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
const suggestion = obj;
|
|
658
|
+
if (typeof suggestion["aspect"] !== "string" || typeof suggestion["issue"] !== "string" || typeof suggestion["recommendation"] !== "string" || typeof suggestion["severity"] !== "string") {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
if (!VALID_ASPECTS.includes(suggestion["aspect"])) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
if (!VALID_SEVERITIES.includes(suggestion["severity"])) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
if (suggestion["location"] !== void 0 && typeof suggestion["location"] !== "string") {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
function parseSuggestions(response) {
|
|
673
|
+
const jsonString = extractJson(response);
|
|
674
|
+
if (!jsonString) {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const parsed = JSON.parse(jsonString);
|
|
679
|
+
if (!Array.isArray(parsed)) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
const validSuggestions = parsed.filter(isValidSuggestion);
|
|
683
|
+
if (parsed.length > 0 && validSuggestions.length === 0) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
return validSuggestions;
|
|
687
|
+
} catch {
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/generator.ts
|
|
693
|
+
var DslGenerator = class {
|
|
694
|
+
providerType;
|
|
695
|
+
provider;
|
|
696
|
+
security;
|
|
697
|
+
context;
|
|
698
|
+
constructor(options) {
|
|
699
|
+
this.providerType = options.provider;
|
|
700
|
+
this.provider = options.providerInstance ?? getProvider(options.provider);
|
|
701
|
+
this.security = options.security ?? {};
|
|
702
|
+
this.context = options.context ?? {};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Generate DSL from a prompt
|
|
706
|
+
*/
|
|
707
|
+
async generate(options) {
|
|
708
|
+
const { prompt, output, context, security } = options;
|
|
709
|
+
const mergedContext = {
|
|
710
|
+
...this.context,
|
|
711
|
+
...context
|
|
712
|
+
};
|
|
713
|
+
const mergedSecurity = {
|
|
714
|
+
...this.security,
|
|
715
|
+
...security
|
|
716
|
+
};
|
|
717
|
+
let userPrompt;
|
|
718
|
+
let systemPrompt;
|
|
719
|
+
switch (output) {
|
|
720
|
+
case "component":
|
|
721
|
+
userPrompt = buildComponentPrompt({
|
|
722
|
+
description: prompt,
|
|
723
|
+
context: mergedContext
|
|
724
|
+
});
|
|
725
|
+
systemPrompt = COMPONENT_SYSTEM_PROMPT;
|
|
726
|
+
break;
|
|
727
|
+
case "view":
|
|
728
|
+
userPrompt = buildViewPrompt({
|
|
729
|
+
description: prompt,
|
|
730
|
+
context: mergedContext
|
|
731
|
+
});
|
|
732
|
+
systemPrompt = VIEW_SYSTEM_PROMPT;
|
|
733
|
+
break;
|
|
734
|
+
case "suggestion":
|
|
735
|
+
userPrompt = buildSuggestPrompt({
|
|
736
|
+
dsl: {},
|
|
737
|
+
aspect: "accessibility"
|
|
738
|
+
});
|
|
739
|
+
systemPrompt = SUGGEST_SYSTEM_PROMPT;
|
|
740
|
+
break;
|
|
741
|
+
default:
|
|
742
|
+
userPrompt = prompt;
|
|
743
|
+
systemPrompt = COMPONENT_SYSTEM_PROMPT;
|
|
744
|
+
}
|
|
745
|
+
const response = await this.provider.generate(userPrompt, {
|
|
746
|
+
systemPrompt
|
|
747
|
+
});
|
|
748
|
+
const raw = response.content;
|
|
749
|
+
const dsl = this.parseResponse(raw);
|
|
750
|
+
const validationResult = this.validate(dsl, mergedSecurity);
|
|
751
|
+
if (validationResult.valid) {
|
|
752
|
+
return {
|
|
753
|
+
dsl,
|
|
754
|
+
raw,
|
|
755
|
+
validated: true
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
dsl,
|
|
760
|
+
raw,
|
|
761
|
+
validated: false,
|
|
762
|
+
errors: validationResult.errors
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Validate DSL against security rules
|
|
767
|
+
*/
|
|
768
|
+
validate(dsl, securityOverrides) {
|
|
769
|
+
const mergedSecurity = {
|
|
770
|
+
...this.security,
|
|
771
|
+
...securityOverrides
|
|
772
|
+
};
|
|
773
|
+
const result = validateDsl(dsl, {
|
|
774
|
+
security: mergedSecurity
|
|
775
|
+
});
|
|
776
|
+
return {
|
|
777
|
+
valid: result.valid,
|
|
778
|
+
errors: result.errors.map((e) => e.message)
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Parse AI response and extract JSON
|
|
783
|
+
*/
|
|
784
|
+
parseResponse(response) {
|
|
785
|
+
const trimmed = response.trim();
|
|
786
|
+
if (trimmed === "") {
|
|
787
|
+
throw new Error("Empty response");
|
|
788
|
+
}
|
|
789
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
790
|
+
const jsonString = codeBlockMatch?.[1]?.trim() ?? trimmed;
|
|
791
|
+
let parsed;
|
|
792
|
+
try {
|
|
793
|
+
parsed = JSON.parse(jsonString);
|
|
794
|
+
} catch {
|
|
795
|
+
throw new Error("Invalid JSON in response");
|
|
796
|
+
}
|
|
797
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
798
|
+
throw new Error("Response must be a JSON object");
|
|
799
|
+
}
|
|
800
|
+
return parsed;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
function createDslGenerator(options) {
|
|
804
|
+
return new DslGenerator(options);
|
|
805
|
+
}
|
|
806
|
+
export {
|
|
807
|
+
AI_OUTPUT_TYPES,
|
|
808
|
+
AI_PROVIDERS,
|
|
809
|
+
AiError,
|
|
810
|
+
AnthropicProvider,
|
|
811
|
+
BaseProvider,
|
|
812
|
+
COMPONENT_SYSTEM_PROMPT,
|
|
813
|
+
DslGenerator,
|
|
814
|
+
FORBIDDEN_ACTIONS,
|
|
815
|
+
FORBIDDEN_TAGS,
|
|
816
|
+
FORBIDDEN_URL_SCHEMES,
|
|
817
|
+
OpenAIProvider,
|
|
818
|
+
RESTRICTED_ACTIONS,
|
|
819
|
+
SUGGEST_SYSTEM_PROMPT,
|
|
820
|
+
SecurityError,
|
|
821
|
+
VIEW_SYSTEM_PROMPT,
|
|
822
|
+
ValidationError,
|
|
823
|
+
buildComponentPrompt,
|
|
824
|
+
buildSuggestPrompt,
|
|
825
|
+
buildViewPrompt,
|
|
826
|
+
createAnthropicProvider,
|
|
827
|
+
createDslGenerator,
|
|
828
|
+
createOpenAIProvider,
|
|
829
|
+
createProviderFactory,
|
|
830
|
+
getProvider,
|
|
831
|
+
isForbiddenAction,
|
|
832
|
+
isForbiddenScheme,
|
|
833
|
+
isForbiddenTag,
|
|
834
|
+
isRestrictedAction,
|
|
835
|
+
parseSuggestions,
|
|
836
|
+
validateActions,
|
|
837
|
+
validateDsl,
|
|
838
|
+
validateNode,
|
|
839
|
+
validateUrl
|
|
840
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@constela/ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI provider abstraction layer for Constela DSL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
19
|
+
"openai": "^4.96.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.10.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.3.0",
|
|
25
|
+
"vitest": "^2.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@constela/core": "0.16.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
36
|
+
"type-check": "tsc --noEmit",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"clean": "rm -rf dist"
|
|
40
|
+
}
|
|
41
|
+
}
|