@pedrofariasx/qwenproxy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +292 -0
  3. package/bin/qwenproxy.mjs +11 -0
  4. package/package.json +56 -0
  5. package/src/api/models.ts +183 -0
  6. package/src/api/server.ts +126 -0
  7. package/src/cache/memory-cache.ts +186 -0
  8. package/src/core/account-manager.ts +132 -0
  9. package/src/core/accounts.ts +78 -0
  10. package/src/core/config.ts +91 -0
  11. package/src/core/database.ts +92 -0
  12. package/src/core/logger.ts +96 -0
  13. package/src/core/metrics.ts +169 -0
  14. package/src/core/model-registry.ts +30 -0
  15. package/src/core/stream-registry.ts +40 -0
  16. package/src/core/watchdog.ts +130 -0
  17. package/src/index.ts +7 -0
  18. package/src/linter/extraction-engine.ts +165 -0
  19. package/src/linter/index.ts +258 -0
  20. package/src/linter/repair-normalize.ts +245 -0
  21. package/src/linter/safety-gate.ts +219 -0
  22. package/src/linter/streaming-state-machine.ts +252 -0
  23. package/src/linter/structural-parser.ts +352 -0
  24. package/src/linter/types.ts +74 -0
  25. package/src/login.ts +228 -0
  26. package/src/routes/chat.ts +801 -0
  27. package/src/routes/upload.ts +700 -0
  28. package/src/services/playwright.ts +778 -0
  29. package/src/services/qwen.ts +500 -0
  30. package/src/tests/advanced.test.ts +227 -0
  31. package/src/tests/agenticStress.test.ts +360 -0
  32. package/src/tests/concurrency.test.ts +103 -0
  33. package/src/tests/concurrentChat.test.ts +71 -0
  34. package/src/tests/delta.test.ts +63 -0
  35. package/src/tests/index.test.ts +356 -0
  36. package/src/tests/jsonFix.test.ts +98 -0
  37. package/src/tests/linter.test.ts +151 -0
  38. package/src/tests/parallel.test.ts +42 -0
  39. package/src/tests/parser.test.ts +89 -0
  40. package/src/tests/rotation.test.ts +45 -0
  41. package/src/tests/streamingOptimizations.test.ts +328 -0
  42. package/src/tests/structureVerification.test.ts +176 -0
  43. package/src/tools/ast.ts +15 -0
  44. package/src/tools/coercion.ts +67 -0
  45. package/src/tools/confidence.ts +48 -0
  46. package/src/tools/detector.ts +40 -0
  47. package/src/tools/executor.ts +236 -0
  48. package/src/tools/parser.ts +446 -0
  49. package/src/tools/pipeline.ts +122 -0
  50. package/src/tools/registry-runtime.ts +34 -0
  51. package/src/tools/registry.ts +142 -0
  52. package/src/tools/repair.ts +42 -0
  53. package/src/tools/schema.ts +285 -0
  54. package/src/tools/types.ts +104 -0
  55. package/src/tools/validator.ts +33 -0
  56. package/src/utils/context-truncation.ts +61 -0
  57. package/src/utils/json.ts +114 -0
  58. package/src/utils/qwen-stream-parser.ts +286 -0
  59. package/src/utils/types.ts +101 -0
@@ -0,0 +1,219 @@
1
+ /*
2
+ * Layer 5: Validation + Safety Gate
3
+ */
4
+
5
+ import type { CanonicalToolCall, ToolCallSource, SecurityViolation, ToolDefinition } from './types'
6
+
7
+ export interface ValidationReport {
8
+ isValid: boolean
9
+ violations: SecurityViolation[]
10
+ confidence: number
11
+ warnings: string[]
12
+ canExecute: boolean
13
+ }
14
+
15
+ export class SafetyGate {
16
+ private registry: Record<string, ToolDefinition> = {}
17
+
18
+ private readonly dangerousPatterns = [
19
+ /\$\(/g, /\$\{/g, /`/g,
20
+ /;\s*(rm|cat|ls|whoami|id|curl|wget|nc|ncat)\b/gi,
21
+ /\|\s*(sh|bash|zsh|csh|python|node|perl)\b/gi,
22
+ /&&\s*(rm|cat|whoami)/gi,
23
+ /exec\s*\(/gi, /eval\s*\(/gi, /\beval\s*\(/gi,
24
+ /system\s*\(/gi, /passthru\s*\(/gi, /shell_exec\s*\(/gi,
25
+ /proc\s*\(/gi, /subprocess\s*\./gi, /spawn\s*\(/gi,
26
+ /execSync\s*\(/gi, /child_process/gi,
27
+ ]
28
+
29
+ private readonly ssrfPatterns = [
30
+ /http[s]?:\/\/(?:127\.0\.0\.1|0\.0\.0\.0|\[::1\]|localhost|169\.254\.\d+\.\d+|metadata\.google)/i,
31
+ /gopher:\/\//i, /file:\/\//i, /ftp:\/\//i,
32
+ /\b7777\b|\b9090\b|\b3389\b|\b22\b/,
33
+ ]
34
+
35
+ private readonly promptInjectionPatterns = [
36
+ /ignore\s+(all\s+)?previous\s+instructions/i, /disregard\s+(all\s+)?previous/i,
37
+ /jailbreak/i, /you\s+are\s+now\s+DAN/i, /pretend\s+you\s+are/i,
38
+ /act\s+as\s+if/i, /your\s+new\s+instructions/i, /developer\s+mode/i,
39
+ /list\s+all\s+files/i, /cat\s+\/etc/i,
40
+ ]
41
+
42
+ private readonly encodedPatterns = [
43
+ /%3Cscript/i, /javascript:/i, /\\x[0-9a-fA-F]{2}/, /\\u[0-9a-fA-F]{4}/,
44
+ /&#[0-9]+;/i, /data:text\/html/i,
45
+ ]
46
+
47
+ registerTool(name: string, definition: Partial<ToolDefinition>): void {
48
+ this.registry[name] = {
49
+ name,
50
+ description: definition.description ?? '',
51
+ parameters: definition.parameters ?? {},
52
+ required: definition.required ?? [],
53
+ strict: definition.strict ?? false,
54
+ }
55
+ }
56
+
57
+ registerRegistry(registry: Record<string, ToolDefinition>): void {
58
+ this.registry = registry
59
+ }
60
+
61
+ validate(call: CanonicalToolCall, sourceHint: ToolCallSource): ValidationReport {
62
+ const violations: SecurityViolation[] = []
63
+ const warnings: string[] = []
64
+ let confidence = call.meta?.confidence ?? 0.5
65
+
66
+ const semanticResult = this.validateSemantics(call)
67
+ violations.push(...semanticResult.violations)
68
+ warnings.push(...semanticResult.warnings)
69
+ confidence = Math.min(confidence, semanticResult.confidence)
70
+
71
+ const securityResult = this.validateSecurity(call)
72
+ violations.push(...securityResult.violations)
73
+ warnings.push(...securityResult.warnings)
74
+
75
+ warnings.push(...this.validateTypes(call).warnings)
76
+
77
+ const registryResult = this.validateRegistry(call)
78
+ warnings.push(...registryResult.warnings)
79
+ confidence = Math.min(confidence, registryResult.confidence)
80
+
81
+ const hasBlockingViolations = violations.some(v =>
82
+ v.type === 'shell_injection' || v.type === 'filesystem_access' || v.type === 'prompt_injection'
83
+ )
84
+
85
+ confidence = Math.max(0.1, Math.min(1.0, confidence))
86
+
87
+ return {
88
+ isValid: !hasBlockingViolations && violations.length === 0,
89
+ violations,
90
+ confidence,
91
+ warnings,
92
+ canExecute: !hasBlockingViolations,
93
+ }
94
+ }
95
+
96
+ private validateSemantics(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[]; confidence: number } {
97
+ const violations: SecurityViolation[] = []
98
+ const warnings: string[] = []
99
+ let confidence = 1.0
100
+
101
+ if (!call.tool || call.tool.trim().length === 0) {
102
+ violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is empty' })
103
+ confidence -= 0.5
104
+ }
105
+ if (typeof call.tool !== 'string') {
106
+ violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is not a string' })
107
+ confidence -= 0.4
108
+ }
109
+ if (call.tool && /[ /\\]/.test(call.tool)) {
110
+ warnings.push(`Suspicious tool name: "${call.tool}"`)
111
+ confidence -= 0.1
112
+ }
113
+ if (typeof call.input !== 'object' || call.input === null || Array.isArray(call.input)) {
114
+ violations.push({ type: 'shell_injection', field: 'input', value: String(call.input), detail: 'Input is not a valid object' })
115
+ confidence -= 0.4
116
+ }
117
+ for (const [key, value] of Object.entries(call.input)) {
118
+ if (typeof value === 'string' && value.trim().length === 0) {
119
+ warnings.push(`Empty string parameter: "${key}"`)
120
+ confidence -= 0.05
121
+ }
122
+ }
123
+
124
+ return { violations, warnings, confidence: Math.max(0, confidence) }
125
+ }
126
+
127
+ private validateSecurity(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[] } {
128
+ const violations: SecurityViolation[] = []
129
+ const warnings: string[] = []
130
+ const inputString = JSON.stringify(call.input)
131
+
132
+ for (const pattern of this.dangerousPatterns) {
133
+ pattern.lastIndex = 0
134
+ if (pattern.test(inputString)) {
135
+ violations.push({ type: 'shell_injection', field: 'input', value: inputString.slice(0, 100), detail: `Potential shell injection pattern detected` })
136
+ break
137
+ }
138
+ }
139
+
140
+ for (const field of Object.values(call.input)) {
141
+ const str = String(field ?? '')
142
+ for (const pattern of this.ssrfPatterns) {
143
+ pattern.lastIndex = 0
144
+ if (pattern.test(str)) {
145
+ violations.push({ type: 'ssrf', field: 'input', value: str.slice(0, 200), detail: `SSRF pattern detected` })
146
+ break
147
+ }
148
+ }
149
+ }
150
+
151
+ const combinedInput = Object.values(call.input)
152
+ .map(v => typeof v === 'string' ? v : JSON.stringify(v))
153
+ .join(' ')
154
+
155
+ for (const pattern of this.promptInjectionPatterns) {
156
+ pattern.lastIndex = 0
157
+ if (pattern.test(combinedInput)) {
158
+ violations.push({ type: 'prompt_injection', field: 'input', value: combinedInput.slice(0, 200), detail: `Prompt injection pattern detected` })
159
+ break
160
+ }
161
+ }
162
+
163
+ for (const pattern of this.encodedPatterns) {
164
+ pattern.lastIndex = 0
165
+ if (pattern.test(combinedInput)) {
166
+ violations.push({ type: 'encoded_payload', field: 'input', value: combinedInput.slice(0, 200), detail: `Encoded payload detected` })
167
+ break
168
+ }
169
+ }
170
+
171
+ return { violations, warnings }
172
+ }
173
+
174
+ private validateTypes(call: CanonicalToolCall): { warnings: string[] } {
175
+ const warnings: string[] = []
176
+ for (const [key, value] of Object.entries(call.input)) {
177
+ const type = typeof value
178
+ if (value !== null && typeof value !== 'undefined' && typeof value !== 'function' && typeof value !== 'symbol' && typeof value !== 'bigint' && typeof value !== 'object') {
179
+ warnings.push(`Unexpected type for parameter "${key}": ${type}`)
180
+ }
181
+ }
182
+ return { warnings }
183
+ }
184
+
185
+ private validateRegistry(call: CanonicalToolCall): { warnings: string[]; confidence: number } {
186
+ const warnings: string[] = []
187
+ let confidence = 1.0
188
+
189
+ const toolDef = this.registry[call.tool]
190
+ if (!toolDef) {
191
+ warnings.push(`Tool "${call.tool}" not found in registry`)
192
+ confidence -= 0.3
193
+ return { warnings, confidence: Math.max(0.1, confidence) }
194
+ }
195
+
196
+ if (toolDef.required && Array.isArray(toolDef.required)) {
197
+ for (const requiredField of toolDef.required) {
198
+ if (!(requiredField in call.input) || call.input[requiredField] === undefined) {
199
+ warnings.push(`Missing required field: "${requiredField}" for tool "${call.tool}"`)
200
+ confidence -= 0.2
201
+ }
202
+ }
203
+ }
204
+
205
+ if (toolDef.strict && toolDef.parameters) {
206
+ for (const [param, expectedType] of Object.entries(toolDef.parameters)) {
207
+ if (param in call.input && call.input[param] !== undefined) {
208
+ const actualType = typeof call.input[param]
209
+ if (actualType !== String(expectedType).toLowerCase()) {
210
+ warnings.push(`Type mismatch for "${param}": expected ${String(expectedType)}, got ${actualType}`)
211
+ confidence -= 0.1
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ return { warnings, confidence: Math.max(0.1, confidence) }
218
+ }
219
+ }
@@ -0,0 +1,252 @@
1
+ /*
2
+ * Layer 1: Streaming State Machine
3
+ * Incremental parser that processes token-by-token chunks
4
+ * Maintains structural state without requiring complete input
5
+ */
6
+
7
+ import type { ParserState } from './types'
8
+
9
+ export interface StreamChunkResult {
10
+ buffer: string
11
+ triggersExtraction: boolean
12
+ inProgress: boolean
13
+ }
14
+
15
+ export class StreamingStateMachine {
16
+ private state: ParserState = {
17
+ buffer: '',
18
+ insideCodeBlock: false,
19
+ braceDepth: 0,
20
+ bracketDepth: 0,
21
+ inString: false,
22
+ escapeNext: false,
23
+ potentialToolStart: false,
24
+ }
25
+
26
+ private lastChunkEndsWithJson: boolean = false
27
+
28
+ reset(): void {
29
+ this.state = {
30
+ buffer: '',
31
+ insideCodeBlock: false,
32
+ braceDepth: 0,
33
+ bracketDepth: 0,
34
+ inString: false,
35
+ escapeNext: false,
36
+ potentialToolStart: false,
37
+ }
38
+ this.lastChunkEndsWithJson = false
39
+ }
40
+
41
+ getState(): ParserState {
42
+ return { ...this.state }
43
+ }
44
+
45
+ isInsideStructuredValue(): boolean {
46
+ return this.state.braceDepth > 0 || this.state.bracketDepth > 0
47
+ }
48
+
49
+ push(chunk: string): StreamChunkResult {
50
+ this.state.buffer += chunk
51
+
52
+ for (let i = 0; i < chunk.length; i++) {
53
+ const char = chunk[i]
54
+ this.processChar(char)
55
+ }
56
+
57
+ const triggersExtraction = this.shouldAttemptExtraction()
58
+ const inProgress = this.isInsideStructuredValue() || this.looksLikeReAct(chunk)
59
+
60
+ return {
61
+ buffer: this.state.buffer,
62
+ triggersExtraction,
63
+ inProgress,
64
+ }
65
+ }
66
+
67
+ private processChar(char: string): void {
68
+ const { inString, escapeNext } = this.state
69
+
70
+ if (escapeNext) {
71
+ this.state.escapeNext = false
72
+ return
73
+ }
74
+
75
+ if (char === '\\' && inString) {
76
+ this.state.escapeNext = true
77
+ return
78
+ }
79
+
80
+ if (!inString) {
81
+ if (char === '"') {
82
+ this.state.inString = true
83
+ return
84
+ }
85
+
86
+ if (char === '{') {
87
+ this.state.braceDepth++
88
+ this.state.potentialToolStart = true
89
+ return
90
+ }
91
+
92
+ if (char === '}') {
93
+ if (this.state.braceDepth > 0) this.state.braceDepth--
94
+ this.updatePotentialToolStart()
95
+ return
96
+ }
97
+
98
+ if (char === '[') {
99
+ this.state.bracketDepth++
100
+ return
101
+ }
102
+
103
+ if (char === ']') {
104
+ if (this.state.bracketDepth > 0) this.state.bracketDepth--
105
+ return
106
+ }
107
+
108
+ if (char === '`') {
109
+ this.state.insideCodeBlock = !this.state.insideCodeBlock
110
+ return
111
+ }
112
+ } else {
113
+ if (char === '"') {
114
+ this.state.inString = false
115
+ return
116
+ }
117
+ }
118
+ }
119
+
120
+ private updatePotentialToolStart(): void {
121
+ const { buffer, braceDepth } = this.state
122
+ if (braceDepth > 0) return
123
+
124
+ const trimmed = buffer.trimEnd()
125
+ if (trimmed.length === 0) {
126
+ this.state.potentialToolStart = false
127
+ return
128
+ }
129
+
130
+ const lastBlock = this.extractLastBlock(trimmed)
131
+ if (lastBlock) {
132
+ this.state.potentialToolStart = true
133
+ }
134
+ }
135
+
136
+ private extractLastBlock(text: string): string | null {
137
+ let depth = 0
138
+ let start = -1
139
+
140
+ for (let i = text.length - 1; i >= 0; i--) {
141
+ const char = text[i]
142
+ if (char === '}') {
143
+ if (depth === 0) start = i
144
+ depth++
145
+ } else if (char === '{') {
146
+ depth--
147
+ if (depth === 0 && start !== -1) {
148
+ return text.slice(i, start + 1)
149
+ }
150
+ }
151
+ }
152
+
153
+ return null
154
+ }
155
+
156
+ private looksLikeReAct(chunk: string): boolean {
157
+ const markers = ['Action:', 'Action Input:', 'Thought:']
158
+
159
+ for (const marker of markers) {
160
+ if (chunk.includes(marker)) return true
161
+ }
162
+
163
+ return false
164
+ }
165
+
166
+ private shouldAttemptExtraction(): boolean {
167
+ const { braceDepth, buffer } = this.state
168
+
169
+ if (braceDepth === 0 && buffer.trim().length > 0) {
170
+ const trimmed = buffer.trimEnd()
171
+ return trimmed.endsWith('}') || trimmed.endsWith(']')
172
+ }
173
+
174
+ return false
175
+ }
176
+
177
+ extractCompleteSpans(): string[] {
178
+ const spans: string[] = []
179
+ const { buffer, braceDepth } = this.state
180
+
181
+ if (braceDepth !== 0) return spans
182
+
183
+ const trimmed = buffer.trim()
184
+ if (trimmed.length === 0) return spans
185
+
186
+ // Extract JSON objects/arrays
187
+ let depth = 0
188
+ let start = -1
189
+ let inString = false
190
+ let escape = false
191
+
192
+ for (let i = 0; i < trimmed.length; i++) {
193
+ const char = trimmed[i]
194
+
195
+ if (escape) {
196
+ escape = false
197
+ continue
198
+ }
199
+
200
+ if (char === '\\' && inString) {
201
+ escape = true
202
+ continue
203
+ }
204
+
205
+ if (char === '"' && !escape) {
206
+ inString = !inString
207
+ continue
208
+ }
209
+
210
+ if (inString) continue
211
+
212
+ if (char === '{' || char === '[') {
213
+ if (depth === 0) start = i
214
+ depth++
215
+ } else if (char === '}' || char === ']') {
216
+ depth--
217
+ if (depth === 0 && start !== -1) {
218
+ spans.push(trimmed.slice(start, i + 1))
219
+ start = -1
220
+ }
221
+ }
222
+ }
223
+
224
+ // Check for ReAct patterns
225
+ const reactMatch = trimmed.match(/Action:\s*(\w+)\s*\n?Action Input:\s*(\{[\s\S]*\})/) ||
226
+ trimmed.match(/Action:\s*(\w+)\s*Action Input:\s*(\{[\s\S]*\})/)
227
+
228
+ if (reactMatch && !spans.includes(reactMatch[2])) {
229
+ spans.push(reactMatch[2])
230
+ }
231
+
232
+ return spans
233
+ }
234
+
235
+ hasCompleteContent(): boolean {
236
+ const { braceDepth, buffer } = this.state
237
+
238
+ if (braceDepth !== 0) return false
239
+
240
+ const trimmed = buffer.trim()
241
+ return trimmed.includes('{') || trimmed.includes('[') ||
242
+ /Action:\s*\w+\s*\n?Action Input:/.test(trimmed)
243
+ }
244
+
245
+ getBuffer(): string {
246
+ return this.state.buffer
247
+ }
248
+
249
+ setBuffer(buffer: string): void {
250
+ this.state.buffer = buffer
251
+ }
252
+ }