mobilcoder-mcp 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/dist/adapters/cli-adapter.d.ts +13 -0
- package/dist/adapters/cli-adapter.d.ts.map +1 -0
- package/dist/adapters/cli-adapter.js +62 -0
- package/dist/adapters/cli-adapter.js.map +1 -0
- package/dist/agent.d.ts +10 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +63 -0
- package/dist/agent.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-handler.d.ts +3 -0
- package/dist/mcp-handler.d.ts.map +1 -0
- package/dist/mcp-handler.js +317 -0
- package/dist/mcp-handler.js.map +1 -0
- package/dist/security.d.ts +52 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +307 -0
- package/dist/security.js.map +1 -0
- package/dist/tool-detector.d.ts +18 -0
- package/dist/tool-detector.d.ts.map +1 -0
- package/dist/tool-detector.js +130 -0
- package/dist/tool-detector.js.map +1 -0
- package/dist/webrtc.d.ts +20 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +152 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +35 -0
- package/src/adapters/cli-adapter.ts +73 -0
- package/src/agent.ts +71 -0
- package/src/index.ts +162 -0
- package/src/mcp-handler.ts +324 -0
- package/src/security.ts +294 -0
- package/src/tool-detector.ts +110 -0
- package/src/webrtc.ts +156 -0
- package/tsconfig.json +21 -0
package/src/security.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
// Security configuration
|
|
6
|
+
export const SECURITY_CONFIG = {
|
|
7
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
8
|
+
maxRequestsPerMinute: 60,
|
|
9
|
+
maxRequestsPerHour: 1000,
|
|
10
|
+
allowedFileExtensions: ['.ts', '.js', '.jsx', '.tsx', '.json', '.md', '.txt', '.yml', '.yaml', '.env.example'],
|
|
11
|
+
blockedPaths: [
|
|
12
|
+
'.git',
|
|
13
|
+
'node_modules',
|
|
14
|
+
'.env',
|
|
15
|
+
'.env.local',
|
|
16
|
+
'.env.development',
|
|
17
|
+
'.env.production',
|
|
18
|
+
'dist',
|
|
19
|
+
'build',
|
|
20
|
+
'.next',
|
|
21
|
+
'.nuxt',
|
|
22
|
+
'.cache',
|
|
23
|
+
'tmp',
|
|
24
|
+
'temp'
|
|
25
|
+
],
|
|
26
|
+
blockedFilePatterns: [
|
|
27
|
+
/\.key$/,
|
|
28
|
+
/\.pem$/,
|
|
29
|
+
/\.crt$/,
|
|
30
|
+
/\.p12$/,
|
|
31
|
+
/private/i,
|
|
32
|
+
/secret/i,
|
|
33
|
+
/password/i,
|
|
34
|
+
/token/i,
|
|
35
|
+
/\.log$/,
|
|
36
|
+
/\.pid$/,
|
|
37
|
+
/\.lock$/,
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Rate limiting
|
|
42
|
+
class RateLimiter {
|
|
43
|
+
private requests = new Map<string, { count: number; resetTime: number; lastReset: number }>();
|
|
44
|
+
|
|
45
|
+
constructor(private maxRequests: number, private windowMs: number) {}
|
|
46
|
+
|
|
47
|
+
isAllowed(identifier: string): boolean {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const entry = this.requests.get(identifier);
|
|
50
|
+
|
|
51
|
+
if (!entry) {
|
|
52
|
+
this.requests.set(identifier, {
|
|
53
|
+
count: 1,
|
|
54
|
+
resetTime: now + this.windowMs,
|
|
55
|
+
lastReset: now
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Reset if window expired
|
|
61
|
+
if (now > entry.resetTime) {
|
|
62
|
+
entry.count = 1;
|
|
63
|
+
entry.resetTime = now + this.windowMs;
|
|
64
|
+
entry.lastReset = now;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entry.count >= this.maxRequests) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
entry.count++;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cleanup(): void {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
for (const [key, entry] of this.requests.entries()) {
|
|
79
|
+
if (now > entry.resetTime) {
|
|
80
|
+
this.requests.delete(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const rateLimiters = {
|
|
87
|
+
perMinute: new RateLimiter(SECURITY_CONFIG.maxRequestsPerMinute, 60 * 1000),
|
|
88
|
+
perHour: new RateLimiter(SECURITY_CONFIG.maxRequestsPerHour, 60 * 60 * 1000),
|
|
89
|
+
fileOperations: new RateLimiter(30, 60 * 1000),
|
|
90
|
+
commands: new RateLimiter(10, 60 * 1000)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Input validation
|
|
94
|
+
export function validatePath(filePath: string, cwd: string): { valid: boolean; error?: string } {
|
|
95
|
+
// Normalize path
|
|
96
|
+
const normalizedPath = path.normalize(filePath);
|
|
97
|
+
const fullPath = path.resolve(cwd, normalizedPath);
|
|
98
|
+
|
|
99
|
+
// Check for path traversal
|
|
100
|
+
if (!fullPath.startsWith(path.resolve(cwd))) {
|
|
101
|
+
return { valid: false, error: 'Path traversal detected' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check for blocked paths
|
|
105
|
+
const pathParts = normalizedPath.split(path.sep);
|
|
106
|
+
for (const part of pathParts) {
|
|
107
|
+
if (SECURITY_CONFIG.blockedPaths.includes(part)) {
|
|
108
|
+
return { valid: false, error: `Access to ${part} is not allowed` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for blocked file patterns
|
|
113
|
+
for (const pattern of SECURITY_CONFIG.blockedFilePatterns) {
|
|
114
|
+
if (pattern.test(normalizedPath)) {
|
|
115
|
+
return { valid: false, error: 'Access to sensitive files is not allowed' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { valid: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateFile(filePath: string, cwd: string): { valid: boolean; error?: string } {
|
|
123
|
+
const pathValidation = validatePath(filePath, cwd);
|
|
124
|
+
if (!pathValidation.valid) {
|
|
125
|
+
return pathValidation;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check file extension
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
if (!SECURITY_CONFIG.allowedFileExtensions.includes(ext)) {
|
|
131
|
+
return { valid: false, error: `File type ${ext} is not allowed` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check file size if exists
|
|
135
|
+
const fullPath = path.resolve(cwd, filePath);
|
|
136
|
+
try {
|
|
137
|
+
const stats = fs.statSync(fullPath);
|
|
138
|
+
if (stats.size > SECURITY_CONFIG.maxFileSize) {
|
|
139
|
+
return { valid: false, error: 'File too large' };
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// File doesn't exist, that's ok
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { valid: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function validateCommand(command: string): { valid: boolean; error?: string } {
|
|
149
|
+
// Check for dangerous commands
|
|
150
|
+
const dangerousPatterns = [
|
|
151
|
+
/\brm\s+-rf\b/i,
|
|
152
|
+
/\bsudo\b/i,
|
|
153
|
+
/\bsu\s/i,
|
|
154
|
+
/\bchmod\s+777\b/i,
|
|
155
|
+
/\bwget\b|\bcurl\b/i,
|
|
156
|
+
/\bnc\s|\bnetcat\b/i,
|
|
157
|
+
/\bssh\b/i,
|
|
158
|
+
/\bscp\b/i,
|
|
159
|
+
/\brsync\b/i,
|
|
160
|
+
/\bdd\s+if=/i,
|
|
161
|
+
/\bmkfs\b/i,
|
|
162
|
+
/\bfdisk\b/i,
|
|
163
|
+
/\bmount\b/i,
|
|
164
|
+
/\bumount\b/i,
|
|
165
|
+
/\bpasswd\b/i,
|
|
166
|
+
/\bshadow\b/i,
|
|
167
|
+
/\bcrontab\b/i,
|
|
168
|
+
/\bsystemctl\b/i,
|
|
169
|
+
/\bservice\s/i,
|
|
170
|
+
/\bkill\s+-9\b/i,
|
|
171
|
+
/\bkillall\b/i,
|
|
172
|
+
/>\s*\/dev\/null/,
|
|
173
|
+
/>\s*\/dev\/(zero|random|urandom)/,
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const pattern of dangerousPatterns) {
|
|
177
|
+
if (pattern.test(command)) {
|
|
178
|
+
return { valid: false, error: 'Dangerous command detected' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { valid: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sanitization
|
|
186
|
+
export function sanitizeInput(input: string): string {
|
|
187
|
+
return input
|
|
188
|
+
.replace(/[<>]/g, '') // Remove HTML tags
|
|
189
|
+
.replace(/[\x00-\x1f\x7f]/g, '') // Remove control characters
|
|
190
|
+
.replace(/[\r\n\t]/g, ' ') // Replace newlines and tabs
|
|
191
|
+
.trim()
|
|
192
|
+
.substring(0, 1000); // Limit length
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sanitizePath(input: string): string {
|
|
196
|
+
return input
|
|
197
|
+
.replace(/\.\./g, '') // Remove parent directory references
|
|
198
|
+
.replace(/^\//, '') // Remove leading slashes
|
|
199
|
+
.replace(/\/$/, '') // Remove trailing slashes
|
|
200
|
+
.replace(/[<>:"|?*]/g, '') // Remove invalid characters
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Security logging
|
|
205
|
+
export class SecurityLogger {
|
|
206
|
+
private static instance: SecurityLogger;
|
|
207
|
+
private logFile: string;
|
|
208
|
+
|
|
209
|
+
private constructor() {
|
|
210
|
+
this.logFile = path.join(process.cwd(), '.security.log');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static getInstance(): SecurityLogger {
|
|
214
|
+
if (!SecurityLogger.instance) {
|
|
215
|
+
SecurityLogger.instance = new SecurityLogger();
|
|
216
|
+
}
|
|
217
|
+
return SecurityLogger.instance;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
log(event: string, details: any, severity: 'low' | 'medium' | 'high' = 'medium'): void {
|
|
221
|
+
const logEntry = {
|
|
222
|
+
timestamp: new Date().toISOString(),
|
|
223
|
+
event,
|
|
224
|
+
details,
|
|
225
|
+
severity,
|
|
226
|
+
pid: process.pid,
|
|
227
|
+
user: process.env.USER || 'unknown'
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const logLine = JSON.stringify(logEntry) + '\n';
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
fs.appendFileSync(this.logFile, logLine);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Failed to write security log:', error);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Also log to console for immediate visibility
|
|
239
|
+
if (severity === 'high') {
|
|
240
|
+
console.error('🚨 SECURITY ALERT:', logEntry);
|
|
241
|
+
} else if (severity === 'medium') {
|
|
242
|
+
console.warn('⚠️ SECURITY WARNING:', logEntry);
|
|
243
|
+
} else {
|
|
244
|
+
console.log('ℹ️ SECURITY INFO:', logEntry);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logBlockedCommand(command: string, reason: string): void {
|
|
249
|
+
this.log('blocked_command', { command, reason }, 'high');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
logPathTraversal(attemptedPath: string, resolvedPath: string): void {
|
|
253
|
+
this.log('path_traversal', { attemptedPath, resolvedPath }, 'high');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
logRateLimitExceeded(identifier: string, operation: string): void {
|
|
257
|
+
this.log('rate_limit_exceeded', { identifier, operation }, 'medium');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
logSuspiciousActivity(activity: string, details: any): void {
|
|
261
|
+
this.log('suspicious_activity', { activity, details }, 'medium');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const securityLogger = SecurityLogger.getInstance();
|
|
266
|
+
|
|
267
|
+
// Cleanup old rate limit entries periodically
|
|
268
|
+
setInterval(() => {
|
|
269
|
+
rateLimiters.perMinute.cleanup();
|
|
270
|
+
rateLimiters.perHour.cleanup();
|
|
271
|
+
rateLimiters.fileOperations.cleanup();
|
|
272
|
+
rateLimiters.commands.cleanup();
|
|
273
|
+
}, 5 * 60 * 1000); // Every 5 minutes
|
|
274
|
+
|
|
275
|
+
// Generate secure tokens
|
|
276
|
+
export function generateSecureToken(length: number = 32): string {
|
|
277
|
+
return crypto.randomBytes(length).toString('hex');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate session tokens
|
|
281
|
+
export function validateSessionToken(token: string): boolean {
|
|
282
|
+
// Basic token validation
|
|
283
|
+
if (!token || typeof token !== 'string') {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check length
|
|
288
|
+
if (token.length < 16 || token.length > 128) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check format (hex)
|
|
293
|
+
return /^[a-f0-9]+$/.test(token);
|
|
294
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
export interface DetectedTool {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
path: string;
|
|
14
|
+
isInstalled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ToolDetector {
|
|
18
|
+
async detectAll(): Promise<DetectedTool[]> {
|
|
19
|
+
const tools = [
|
|
20
|
+
this.checkClaude(),
|
|
21
|
+
this.checkGemini(),
|
|
22
|
+
this.checkQoder(),
|
|
23
|
+
this.checkKiro(),
|
|
24
|
+
this.checkAider(),
|
|
25
|
+
this.checkCursor()
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const results = await Promise.all(tools);
|
|
29
|
+
return results.filter(t => t.isInstalled);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async checkCommand(command: string): Promise<string | null> {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execAsync(`${command} --version`);
|
|
35
|
+
return stdout.trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async checkClaude(): Promise<DetectedTool> {
|
|
42
|
+
const version = await this.checkCommand('claude');
|
|
43
|
+
return {
|
|
44
|
+
id: 'claude',
|
|
45
|
+
name: 'Claude Code',
|
|
46
|
+
version: version || '',
|
|
47
|
+
path: '',
|
|
48
|
+
isInstalled: !!version
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async checkGemini(): Promise<DetectedTool> {
|
|
53
|
+
const version = await this.checkCommand('gemini');
|
|
54
|
+
return {
|
|
55
|
+
id: 'gemini',
|
|
56
|
+
name: 'Gemini CLI',
|
|
57
|
+
version: version || '',
|
|
58
|
+
path: '',
|
|
59
|
+
isInstalled: !!version
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async checkQoder(): Promise<DetectedTool> {
|
|
64
|
+
const version = await this.checkCommand('qoder');
|
|
65
|
+
return {
|
|
66
|
+
id: 'qoder',
|
|
67
|
+
name: 'Qoder',
|
|
68
|
+
version: version || '',
|
|
69
|
+
path: '',
|
|
70
|
+
isInstalled: !!version
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async checkKiro(): Promise<DetectedTool> {
|
|
75
|
+
const version = await this.checkCommand('kiro');
|
|
76
|
+
return {
|
|
77
|
+
id: 'kiro',
|
|
78
|
+
name: 'Kiro',
|
|
79
|
+
version: version || '',
|
|
80
|
+
path: '',
|
|
81
|
+
isInstalled: !!version
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async checkAider(): Promise<DetectedTool> {
|
|
86
|
+
const version = await this.checkCommand('aider');
|
|
87
|
+
return {
|
|
88
|
+
id: 'aider',
|
|
89
|
+
name: 'Aider',
|
|
90
|
+
version: version || '',
|
|
91
|
+
path: '',
|
|
92
|
+
isInstalled: !!version
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async checkCursor(): Promise<DetectedTool> {
|
|
97
|
+
// Cursor is an app, not typically a CLI command for version checking in the same way
|
|
98
|
+
// We check for config file existence as a proxy for "installed/configured"
|
|
99
|
+
const configPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
100
|
+
const isInstalled = fs.existsSync(configPath);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
id: 'mcp', // Maps to 'mcp' in ToolSelector
|
|
104
|
+
name: 'Cursor',
|
|
105
|
+
version: 'App',
|
|
106
|
+
path: configPath,
|
|
107
|
+
isInstalled
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/webrtc.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import SimplePeer from 'simple-peer';
|
|
2
|
+
|
|
3
|
+
export class WebRTCConnection {
|
|
4
|
+
private peer: SimplePeer.Instance | null = null;
|
|
5
|
+
private code: string;
|
|
6
|
+
private signalingUrl: string;
|
|
7
|
+
private onMessageCallback?: (message: any) => void;
|
|
8
|
+
private onConnectCallback?: () => void;
|
|
9
|
+
private onDisconnectCallback?: () => void;
|
|
10
|
+
private isConnected: boolean = false;
|
|
11
|
+
private pollingInterval?: NodeJS.Timeout;
|
|
12
|
+
|
|
13
|
+
constructor(code: string, signalingUrl: string) {
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.signalingUrl = signalingUrl;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect(): Promise<void> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
try {
|
|
21
|
+
// Desktop acts as answerer (not initiator)
|
|
22
|
+
this.peer = new SimplePeer({
|
|
23
|
+
initiator: false,
|
|
24
|
+
trickle: false,
|
|
25
|
+
config: {
|
|
26
|
+
iceServers: [
|
|
27
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
28
|
+
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.peer.on('signal', async (signal: any) => {
|
|
34
|
+
// Send answer to signaling server
|
|
35
|
+
try {
|
|
36
|
+
await fetch(`${this.signalingUrl}/answer`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ code: this.code, signal })
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Failed to send answer:', error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.peer.on('connect', () => {
|
|
47
|
+
console.log('✅ Connected to mobile device!');
|
|
48
|
+
this.isConnected = true;
|
|
49
|
+
if (this.onConnectCallback) {
|
|
50
|
+
this.onConnectCallback();
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.peer.on('data', (data: any) => {
|
|
56
|
+
try {
|
|
57
|
+
const message = JSON.parse(data.toString());
|
|
58
|
+
if (this.onMessageCallback) {
|
|
59
|
+
this.onMessageCallback(message);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to parse message:', error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.peer.on('error', (error: any) => {
|
|
67
|
+
console.error('WebRTC error:', error);
|
|
68
|
+
this.isConnected = false;
|
|
69
|
+
if (this.onDisconnectCallback) {
|
|
70
|
+
this.onDisconnectCallback();
|
|
71
|
+
}
|
|
72
|
+
reject(error);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.peer.on('close', () => {
|
|
76
|
+
console.log('❌ Connection closed');
|
|
77
|
+
this.isConnected = false;
|
|
78
|
+
if (this.onDisconnectCallback) {
|
|
79
|
+
this.onDisconnectCallback();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Start polling for offer from mobile
|
|
84
|
+
this.startPollingForOffer();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async startPollingForOffer(): Promise<void> {
|
|
92
|
+
const poll = async () => {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${this.signalingUrl}/poll?code=${this.code}`);
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
const data = await response.json() as { signal?: any };
|
|
97
|
+
if (data.signal && this.peer) {
|
|
98
|
+
// Received offer from mobile, signal the peer
|
|
99
|
+
this.peer.signal(data.signal);
|
|
100
|
+
// Stop polling once we got the offer
|
|
101
|
+
if (this.pollingInterval) {
|
|
102
|
+
clearInterval(this.pollingInterval);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Silently handle polling errors
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Poll every 2 seconds
|
|
112
|
+
this.pollingInterval = setInterval(poll, 2000);
|
|
113
|
+
// Initial poll
|
|
114
|
+
await poll();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
send(message: any): void {
|
|
118
|
+
if (this.peer && this.isConnected) {
|
|
119
|
+
try {
|
|
120
|
+
this.peer.send(JSON.stringify(message));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Failed to send message:', error);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
console.warn('Cannot send message: not connected');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onMessage(callback: (message: any) => void): void {
|
|
130
|
+
this.onMessageCallback = callback;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onConnect(callback: () => void): void {
|
|
134
|
+
this.onConnectCallback = callback;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onDisconnect(callback: () => void): void {
|
|
138
|
+
this.onDisconnectCallback = callback;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
disconnect(): void {
|
|
142
|
+
if (this.pollingInterval) {
|
|
143
|
+
clearInterval(this.pollingInterval);
|
|
144
|
+
}
|
|
145
|
+
if (this.peer) {
|
|
146
|
+
this.peer.destroy();
|
|
147
|
+
this.peer = null;
|
|
148
|
+
}
|
|
149
|
+
this.isConnected = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getConnected(): boolean {
|
|
153
|
+
return this.isConnected;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
21
|
+
|