@sumicom/quicksave 0.1.0 → 0.2.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/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/signaling.d.ts.map +1 -1
- package/dist/connection/signaling.js +5 -0
- package/dist/connection/signaling.js.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +25 -13
- package/LICENSE +0 -21
- package/dist/webrtc/connection.d.ts +0 -52
- package/dist/webrtc/connection.d.ts.map +0 -1
- package/dist/webrtc/connection.js +0 -255
- package/dist/webrtc/connection.js.map +0 -1
- package/dist/webrtc/signaling.d.ts +0 -41
- package/dist/webrtc/signaling.d.ts.map +0 -1
- package/dist/webrtc/signaling.js +0 -152
- package/dist/webrtc/signaling.js.map +0 -1
- package/src/ai/commitSummary.ts +0 -199
- package/src/config.ts +0 -97
- package/src/connection/connection.ts +0 -223
- package/src/connection/signaling.ts +0 -172
- package/src/git/operations.test.ts +0 -305
- package/src/git/operations.ts +0 -443
- package/src/handlers/messageHandler.test.ts +0 -294
- package/src/handlers/messageHandler.ts +0 -550
- package/src/index.ts +0 -151
- package/src/types/webrtc.d.ts +0 -38
- package/tsconfig.json +0 -13
- package/vitest.config.ts +0 -9
package/dist/webrtc/signaling.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
import { gzip, gunzip } from 'zlib';
|
|
4
|
-
import { promisify } from 'util';
|
|
5
|
-
const gzipAsync = promisify(gzip);
|
|
6
|
-
const gunzipAsync = promisify(gunzip);
|
|
7
|
-
export class SignalingClient extends EventEmitter {
|
|
8
|
-
ws = null;
|
|
9
|
-
url;
|
|
10
|
-
agentId;
|
|
11
|
-
reconnectAttempts = 0;
|
|
12
|
-
maxReconnectAttempts = 5;
|
|
13
|
-
reconnectDelay = 1000;
|
|
14
|
-
isConnected = false;
|
|
15
|
-
constructor(signalingServer, agentId) {
|
|
16
|
-
super();
|
|
17
|
-
this.url = `${signalingServer}/agent/${agentId}`;
|
|
18
|
-
this.agentId = agentId;
|
|
19
|
-
}
|
|
20
|
-
connect() {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
try {
|
|
23
|
-
this.ws = new WebSocket(this.url);
|
|
24
|
-
this.ws.on('open', () => {
|
|
25
|
-
this.isConnected = true;
|
|
26
|
-
this.reconnectAttempts = 0;
|
|
27
|
-
this.emit('connected');
|
|
28
|
-
resolve();
|
|
29
|
-
});
|
|
30
|
-
this.ws.on('message', async (data) => {
|
|
31
|
-
try {
|
|
32
|
-
const parsed = JSON.parse(data.toString());
|
|
33
|
-
// Handle compressed messages (z = zipped)
|
|
34
|
-
const message = parsed.z
|
|
35
|
-
? JSON.parse(await this.decompress(parsed.z))
|
|
36
|
-
: parsed;
|
|
37
|
-
this.handleMessage(message);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
console.error('Failed to parse signaling message:', error);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
this.ws.on('close', () => {
|
|
44
|
-
this.isConnected = false;
|
|
45
|
-
this.emit('disconnected');
|
|
46
|
-
this.attemptReconnect();
|
|
47
|
-
});
|
|
48
|
-
this.ws.on('error', (error) => {
|
|
49
|
-
this.emit('error', error);
|
|
50
|
-
if (!this.isConnected) {
|
|
51
|
-
reject(error);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
reject(error);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
handleMessage(message) {
|
|
61
|
-
switch (message.type) {
|
|
62
|
-
case 'peer-connected':
|
|
63
|
-
this.emit('peer-connected');
|
|
64
|
-
break;
|
|
65
|
-
case 'peer-offline':
|
|
66
|
-
this.emit('peer-disconnected');
|
|
67
|
-
break;
|
|
68
|
-
case 'offer':
|
|
69
|
-
this.emit('offer', message.payload);
|
|
70
|
-
break;
|
|
71
|
-
case 'answer':
|
|
72
|
-
this.emit('answer', message.payload);
|
|
73
|
-
break;
|
|
74
|
-
case 'ice-candidate':
|
|
75
|
-
this.emit('ice-candidate', message.payload);
|
|
76
|
-
break;
|
|
77
|
-
case 'relay-mode':
|
|
78
|
-
this.emit('relay-mode');
|
|
79
|
-
break;
|
|
80
|
-
case 'relay-data':
|
|
81
|
-
if (typeof message.payload === 'string') {
|
|
82
|
-
this.emit('relay-data', message.payload);
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
case 'bye':
|
|
86
|
-
this.emit('peer-disconnected');
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
sendOffer(sdp) {
|
|
91
|
-
this.send({ type: 'offer', payload: sdp });
|
|
92
|
-
}
|
|
93
|
-
sendAnswer(sdp) {
|
|
94
|
-
this.send({ type: 'answer', payload: sdp });
|
|
95
|
-
}
|
|
96
|
-
sendIceCandidate(candidate) {
|
|
97
|
-
this.send({ type: 'ice-candidate', payload: candidate });
|
|
98
|
-
}
|
|
99
|
-
sendBye() {
|
|
100
|
-
this.send({ type: 'bye' });
|
|
101
|
-
}
|
|
102
|
-
sendRelayData(data) {
|
|
103
|
-
this.send({ type: 'relay-data', payload: data });
|
|
104
|
-
}
|
|
105
|
-
// Gzip compression helpers
|
|
106
|
-
async compress(data) {
|
|
107
|
-
const buffer = await gzipAsync(Buffer.from(data));
|
|
108
|
-
return buffer.toString('base64');
|
|
109
|
-
}
|
|
110
|
-
async decompress(base64) {
|
|
111
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
112
|
-
const decompressed = await gunzipAsync(buffer);
|
|
113
|
-
return decompressed.toString('utf-8');
|
|
114
|
-
}
|
|
115
|
-
send(message) {
|
|
116
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
117
|
-
// Send compressed
|
|
118
|
-
this.compress(JSON.stringify(message)).then((compressed) => {
|
|
119
|
-
this.ws?.send(JSON.stringify({ z: compressed }));
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
attemptReconnect() {
|
|
124
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
125
|
-
console.error('Max reconnect attempts reached');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
this.reconnectAttempts++;
|
|
129
|
-
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
130
|
-
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
131
|
-
setTimeout(() => {
|
|
132
|
-
this.connect().catch((error) => {
|
|
133
|
-
console.error('Reconnect failed:', error);
|
|
134
|
-
});
|
|
135
|
-
}, delay);
|
|
136
|
-
}
|
|
137
|
-
disconnect() {
|
|
138
|
-
this.maxReconnectAttempts = 0; // Prevent reconnection
|
|
139
|
-
if (this.ws) {
|
|
140
|
-
this.sendBye();
|
|
141
|
-
this.ws.close();
|
|
142
|
-
this.ws = null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
getAgentId() {
|
|
146
|
-
return this.agentId;
|
|
147
|
-
}
|
|
148
|
-
getConnectionUrl() {
|
|
149
|
-
return this.url;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
//# sourceMappingURL=signaling.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"signaling.js","sourceRoot":"","sources":["../../src/webrtc/signaling.ts"],"names":[],"mappings":"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"}
|
package/src/ai/commitSummary.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import { createHash } from 'crypto';
|
|
3
|
-
import type { FileDiff, ClaudeModel, TokenUsage } from '@sumicom/quicksave-shared';
|
|
4
|
-
|
|
5
|
-
export interface GenerateSummaryOptions {
|
|
6
|
-
diffs: FileDiff[];
|
|
7
|
-
context?: string;
|
|
8
|
-
model?: ClaudeModel;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface GenerateSummaryResult {
|
|
12
|
-
summary: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
tokenUsage?: TokenUsage;
|
|
15
|
-
cached?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface CacheEntry {
|
|
19
|
-
result: GenerateSummaryResult;
|
|
20
|
-
timestamp: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DEFAULT_MODEL: ClaudeModel = 'claude-haiku-4-5';
|
|
24
|
-
// Max characters per file diff for AI generation (roughly 1KB)
|
|
25
|
-
const MAX_DIFF_CHARS_PER_FILE = 1000;
|
|
26
|
-
// Max total characters for all diffs combined
|
|
27
|
-
const MAX_TOTAL_DIFF_CHARS = 8000;
|
|
28
|
-
// Cache TTL: 5 minutes
|
|
29
|
-
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
30
|
-
|
|
31
|
-
export class CommitSummaryService {
|
|
32
|
-
private client: Anthropic;
|
|
33
|
-
private cache = new Map<string, CacheEntry>();
|
|
34
|
-
private pendingRequests = new Map<string, Promise<GenerateSummaryResult>>();
|
|
35
|
-
|
|
36
|
-
constructor(apiKey: string) {
|
|
37
|
-
this.client = new Anthropic({ apiKey });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async generateSummary(options: GenerateSummaryOptions): Promise<GenerateSummaryResult> {
|
|
41
|
-
const { diffs, context, model = DEFAULT_MODEL } = options;
|
|
42
|
-
|
|
43
|
-
const diffText = this.formatDiffsForPrompt(diffs);
|
|
44
|
-
|
|
45
|
-
if (!diffText.trim()) {
|
|
46
|
-
return { summary: 'Update files' };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Generate cache key from diff content and model
|
|
50
|
-
const cacheKey = this.getCacheKey(diffText, context, model);
|
|
51
|
-
|
|
52
|
-
// Check cache first
|
|
53
|
-
const cached = this.getFromCache(cacheKey);
|
|
54
|
-
if (cached) {
|
|
55
|
-
return { ...cached, cached: true };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check if there's already a pending request for this exact content
|
|
59
|
-
const pending = this.pendingRequests.get(cacheKey);
|
|
60
|
-
if (pending) {
|
|
61
|
-
const result = await pending;
|
|
62
|
-
return { ...result, cached: true };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create the request promise and store it
|
|
66
|
-
const requestPromise = this.executeGeneration(diffText, context, model, cacheKey);
|
|
67
|
-
this.pendingRequests.set(cacheKey, requestPromise);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
return await requestPromise;
|
|
71
|
-
} finally {
|
|
72
|
-
this.pendingRequests.delete(cacheKey);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private async executeGeneration(
|
|
77
|
-
diffText: string,
|
|
78
|
-
context: string | undefined,
|
|
79
|
-
model: ClaudeModel,
|
|
80
|
-
cacheKey: string
|
|
81
|
-
): Promise<GenerateSummaryResult> {
|
|
82
|
-
const prompt = this.buildPrompt(diffText, context);
|
|
83
|
-
|
|
84
|
-
const response = await this.client.messages.create({
|
|
85
|
-
model,
|
|
86
|
-
max_tokens: 500,
|
|
87
|
-
messages: [{ role: 'user', content: prompt }],
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const result = this.parseResponse(response);
|
|
91
|
-
|
|
92
|
-
// Store in cache
|
|
93
|
-
this.cache.set(cacheKey, {
|
|
94
|
-
result,
|
|
95
|
-
timestamp: Date.now(),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private getCacheKey(diffText: string, context: string | undefined, model: ClaudeModel): string {
|
|
102
|
-
const content = `${model}:${context || ''}:${diffText}`;
|
|
103
|
-
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private getFromCache(key: string): GenerateSummaryResult | null {
|
|
107
|
-
const entry = this.cache.get(key);
|
|
108
|
-
if (!entry) return null;
|
|
109
|
-
|
|
110
|
-
// Check if cache is still valid
|
|
111
|
-
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
112
|
-
this.cache.delete(key);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return entry.result;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private formatDiffsForPrompt(diffs: FileDiff[]): string {
|
|
120
|
-
const formattedDiffs: string[] = [];
|
|
121
|
-
let totalChars = 0;
|
|
122
|
-
|
|
123
|
-
for (const diff of diffs) {
|
|
124
|
-
if (diff.isBinary) {
|
|
125
|
-
formattedDiffs.push(`File: ${diff.path}\n[Binary file]`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const hunksContent = diff.hunks.map((h) => h.content).join('\n');
|
|
130
|
-
let fileContent = hunksContent;
|
|
131
|
-
|
|
132
|
-
// Truncate individual file if too large
|
|
133
|
-
if (fileContent.length > MAX_DIFF_CHARS_PER_FILE) {
|
|
134
|
-
fileContent = fileContent.slice(0, MAX_DIFF_CHARS_PER_FILE) + '\n... [truncated]';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const formatted = `File: ${diff.path}\n${fileContent}`;
|
|
138
|
-
|
|
139
|
-
// Check if adding this would exceed total limit
|
|
140
|
-
if (totalChars + formatted.length > MAX_TOTAL_DIFF_CHARS) {
|
|
141
|
-
formattedDiffs.push(`... and ${diffs.length - formattedDiffs.length} more files`);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
formattedDiffs.push(formatted);
|
|
146
|
-
totalChars += formatted.length;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return formattedDiffs.join('\n\n---\n\n');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private buildPrompt(diffText: string, context?: string): string {
|
|
153
|
-
return `You are a helpful assistant that generates concise, descriptive git commit messages.
|
|
154
|
-
|
|
155
|
-
Analyze the following git diff and generate a commit message following these guidelines:
|
|
156
|
-
- Use conventional commit format when appropriate (feat:, fix:, docs:, refactor:, etc.)
|
|
157
|
-
- Keep the summary line under 72 characters
|
|
158
|
-
- Focus on WHAT changed and WHY, not HOW
|
|
159
|
-
- Be specific but concise
|
|
160
|
-
|
|
161
|
-
${context ? `Additional context from the user: ${context}\n\n` : ''}
|
|
162
|
-
Git diff:
|
|
163
|
-
\`\`\`
|
|
164
|
-
${diffText}
|
|
165
|
-
\`\`\`
|
|
166
|
-
|
|
167
|
-
Respond in this exact JSON format:
|
|
168
|
-
{
|
|
169
|
-
"summary": "the commit summary line",
|
|
170
|
-
"description": "optional extended description if the changes are complex"
|
|
171
|
-
}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private parseResponse(response: Anthropic.Message): GenerateSummaryResult {
|
|
175
|
-
const content = response.content[0];
|
|
176
|
-
if (content.type !== 'text') {
|
|
177
|
-
throw new Error('Unexpected response format');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const tokenUsage: TokenUsage = {
|
|
181
|
-
inputTokens: response.usage.input_tokens,
|
|
182
|
-
outputTokens: response.usage.output_tokens,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// Parse JSON from response
|
|
186
|
-
const jsonMatch = content.text.match(/\{[\s\S]*\}/);
|
|
187
|
-
if (!jsonMatch) {
|
|
188
|
-
// Fallback: use the raw text as summary
|
|
189
|
-
return { summary: content.text.trim().slice(0, 72), tokenUsage };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
193
|
-
return {
|
|
194
|
-
summary: parsed.summary || 'Update code',
|
|
195
|
-
description: parsed.description,
|
|
196
|
-
tokenUsage,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { generateAgentKeyPair } from './connection/connection.js';
|
|
5
|
-
import { generateAgentId, type License } from '@sumicom/quicksave-shared';
|
|
6
|
-
|
|
7
|
-
export interface AgentConfig {
|
|
8
|
-
agentId: string;
|
|
9
|
-
keyPair: {
|
|
10
|
-
publicKey: string;
|
|
11
|
-
secretKey: string;
|
|
12
|
-
};
|
|
13
|
-
license?: License;
|
|
14
|
-
signalingServer: string;
|
|
15
|
-
anthropicApiKey?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const CONFIG_DIR = join(homedir(), '.quicksave');
|
|
19
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'agent.json');
|
|
20
|
-
|
|
21
|
-
export function ensureConfigDir(): void {
|
|
22
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
23
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function loadConfig(): AgentConfig | null {
|
|
28
|
-
try {
|
|
29
|
-
if (existsSync(CONFIG_FILE)) {
|
|
30
|
-
const data = readFileSync(CONFIG_FILE, 'utf-8');
|
|
31
|
-
return JSON.parse(data) as AgentConfig;
|
|
32
|
-
}
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('Failed to load config:', error);
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function saveConfig(config: AgentConfig): void {
|
|
40
|
-
ensureConfigDir();
|
|
41
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createDefaultConfig(signalingServer: string): AgentConfig {
|
|
45
|
-
const config: AgentConfig = {
|
|
46
|
-
agentId: generateAgentId(),
|
|
47
|
-
keyPair: generateAgentKeyPair(),
|
|
48
|
-
signalingServer,
|
|
49
|
-
};
|
|
50
|
-
saveConfig(config);
|
|
51
|
-
return config;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getOrCreateConfig(signalingServer: string): AgentConfig {
|
|
55
|
-
let config = loadConfig();
|
|
56
|
-
|
|
57
|
-
if (!config) {
|
|
58
|
-
console.log('No existing config found, generating new identity...');
|
|
59
|
-
config = createDefaultConfig(signalingServer);
|
|
60
|
-
console.log('New agent identity created');
|
|
61
|
-
} else if (config.signalingServer !== signalingServer) {
|
|
62
|
-
// Update signaling server if changed
|
|
63
|
-
config.signalingServer = signalingServer;
|
|
64
|
-
saveConfig(config);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return config;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function addLicense(license: License): void {
|
|
71
|
-
const config = loadConfig();
|
|
72
|
-
if (config) {
|
|
73
|
-
config.license = license;
|
|
74
|
-
saveConfig(config);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getConfigPath(): string {
|
|
79
|
-
return CONFIG_FILE;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Anthropic API Key helpers
|
|
83
|
-
export function getAnthropicApiKey(): string | undefined {
|
|
84
|
-
return loadConfig()?.anthropicApiKey;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function setAnthropicApiKey(apiKey: string): void {
|
|
88
|
-
const config = loadConfig();
|
|
89
|
-
if (config) {
|
|
90
|
-
config.anthropicApiKey = apiKey;
|
|
91
|
-
saveConfig(config);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function hasAnthropicApiKey(): boolean {
|
|
96
|
-
return !!loadConfig()?.anthropicApiKey;
|
|
97
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { gzip, gunzip } from 'zlib';
|
|
3
|
-
import { promisify } from 'util';
|
|
4
|
-
import {
|
|
5
|
-
generateKeyPair,
|
|
6
|
-
encodeKeyPair,
|
|
7
|
-
decodeKeyPair,
|
|
8
|
-
encryptWithSharedSecret,
|
|
9
|
-
decryptWithSharedSecret,
|
|
10
|
-
decryptDEK,
|
|
11
|
-
parseMessage,
|
|
12
|
-
serializeMessage,
|
|
13
|
-
type Message,
|
|
14
|
-
type KeyPair,
|
|
15
|
-
type KeyExchangeV2,
|
|
16
|
-
} from '@sumicom/quicksave-shared';
|
|
17
|
-
import { SignalingClient } from './signaling.js';
|
|
18
|
-
|
|
19
|
-
const gzipAsync = promisify(gzip);
|
|
20
|
-
const gunzipAsync = promisify(gunzip);
|
|
21
|
-
|
|
22
|
-
export interface ConnectionConfig {
|
|
23
|
-
signalingServer: string;
|
|
24
|
-
agentId: string;
|
|
25
|
-
keyPair: { publicKey: string; secretKey: string };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface WebRTCConnectionEvents {
|
|
29
|
-
connected: () => void;
|
|
30
|
-
disconnected: () => void;
|
|
31
|
-
message: (message: Message) => void;
|
|
32
|
-
error: (error: Error) => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class WebRTCConnection extends EventEmitter {
|
|
36
|
-
private config: ConnectionConfig;
|
|
37
|
-
private signaling: SignalingClient;
|
|
38
|
-
private keyPair: KeyPair;
|
|
39
|
-
// Session DEK for encryption (received encrypted from PWA)
|
|
40
|
-
private sessionDEK: Uint8Array | null = null;
|
|
41
|
-
private isConnected = false;
|
|
42
|
-
|
|
43
|
-
// Key exchange replay protection
|
|
44
|
-
private static readonly KEY_EXCHANGE_MAX_AGE_MS = 60000; // 60 seconds
|
|
45
|
-
|
|
46
|
-
constructor(config: ConnectionConfig) {
|
|
47
|
-
super();
|
|
48
|
-
this.config = config;
|
|
49
|
-
this.keyPair = decodeKeyPair(config.keyPair);
|
|
50
|
-
this.signaling = new SignalingClient(config.signalingServer, config.agentId);
|
|
51
|
-
this.setupSignalingHandlers();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private setupSignalingHandlers(): void {
|
|
55
|
-
this.signaling.on('peer-connected', () => {
|
|
56
|
-
console.log('PWA peer connected, waiting for key exchange...');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
this.signaling.on('data', (data: string) => {
|
|
60
|
-
this.handleDataMessage(data);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
this.signaling.on('peer-disconnected', () => {
|
|
64
|
-
this.handlePeerDisconnected();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.signaling.on('error', (error: Error) => {
|
|
68
|
-
this.emit('error', error);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async start(): Promise<void> {
|
|
73
|
-
console.log('Connecting to signaling server...');
|
|
74
|
-
await this.signaling.connect();
|
|
75
|
-
console.log('Connected to signaling server');
|
|
76
|
-
console.log(`Agent ID: ${this.config.agentId}`);
|
|
77
|
-
console.log(`Public Key: ${this.config.keyPair.publicKey}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private async handleDataMessage(data: string): Promise<void> {
|
|
81
|
-
try {
|
|
82
|
-
// Handle key exchange (uncompressed JSON) vs encrypted messages (base64 compressed)
|
|
83
|
-
if (!this.isKeyExchangeComplete()) {
|
|
84
|
-
// Try to parse as key exchange message
|
|
85
|
-
try {
|
|
86
|
-
const keyExchange = JSON.parse(data);
|
|
87
|
-
if (keyExchange.type === 'key-exchange') {
|
|
88
|
-
await this.handleKeyExchange(keyExchange);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
// Not a key exchange message, ignore until key exchange completes
|
|
93
|
-
console.error('Received message before key exchange');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Post key-exchange: messages are encrypted, then the plaintext was compressed before encryption
|
|
99
|
-
// Decrypt first, then decompress
|
|
100
|
-
const encryptionKey = this.getEncryptionKey();
|
|
101
|
-
if (!encryptionKey) {
|
|
102
|
-
console.error('No encryption key, cannot decrypt message');
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const decrypted = decryptWithSharedSecret(data, encryptionKey);
|
|
107
|
-
const buffer = Buffer.from(decrypted, 'base64');
|
|
108
|
-
const decompressed = await gunzipAsync(buffer);
|
|
109
|
-
const message = parseMessage(decompressed.toString('utf-8'));
|
|
110
|
-
this.emit('message', message);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error('Failed to handle message:', error);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check if key exchange has completed
|
|
118
|
-
*/
|
|
119
|
-
private isKeyExchangeComplete(): boolean {
|
|
120
|
-
return this.sessionDEK !== null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Get the encryption key
|
|
125
|
-
*/
|
|
126
|
-
private getEncryptionKey(): Uint8Array | null {
|
|
127
|
-
return this.sessionDEK;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Handle key exchange message
|
|
132
|
-
*/
|
|
133
|
-
private async handleKeyExchange(message: KeyExchangeV2): Promise<void> {
|
|
134
|
-
// Verify timestamp for replay protection
|
|
135
|
-
const age = Date.now() - message.timestamp;
|
|
136
|
-
if (age > WebRTCConnection.KEY_EXCHANGE_MAX_AGE_MS) {
|
|
137
|
-
console.error(`Key exchange expired (age: ${age}ms)`);
|
|
138
|
-
this.emit('error', new Error('Key exchange expired'));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (age < -5000) {
|
|
143
|
-
// Allow 5 second clock skew into the future
|
|
144
|
-
console.error(`Key exchange timestamp in future (age: ${age}ms)`);
|
|
145
|
-
this.emit('error', new Error('Key exchange timestamp invalid'));
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Decrypt the session DEK
|
|
150
|
-
try {
|
|
151
|
-
this.sessionDEK = decryptDEK(message.encryptedDEK, this.keyPair.secretKey);
|
|
152
|
-
console.log('Key exchange complete, connection encrypted');
|
|
153
|
-
|
|
154
|
-
// Mark as connected
|
|
155
|
-
if (!this.isConnected) {
|
|
156
|
-
this.isConnected = true;
|
|
157
|
-
this.emit('connected');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// V2: Send acknowledgment
|
|
161
|
-
this.sendRaw(
|
|
162
|
-
JSON.stringify({
|
|
163
|
-
type: 'key-exchange-ack',
|
|
164
|
-
version: 2,
|
|
165
|
-
})
|
|
166
|
-
);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error('Failed to decrypt session DEK:', error);
|
|
169
|
-
this.emit('error', new Error('Failed to decrypt session DEK'));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
send(message: Message): void {
|
|
174
|
-
const encryptionKey = this.getEncryptionKey();
|
|
175
|
-
if (!encryptionKey) {
|
|
176
|
-
console.error('No encryption key, cannot encrypt message');
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Compress before encryption for better compression ratio
|
|
181
|
-
const serialized = serializeMessage(message);
|
|
182
|
-
gzipAsync(Buffer.from(serialized)).then((compressed) => {
|
|
183
|
-
const compressedBase64 = compressed.toString('base64');
|
|
184
|
-
const encrypted = encryptWithSharedSecret(compressedBase64, encryptionKey);
|
|
185
|
-
this.signaling.sendData(encrypted);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private sendRaw(data: string): void {
|
|
190
|
-
this.signaling.sendData(data);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private handlePeerDisconnected(): void {
|
|
194
|
-
if (this.isConnected) {
|
|
195
|
-
this.isConnected = false;
|
|
196
|
-
this.emit('disconnected');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Clean up encryption state for next connection
|
|
200
|
-
this.sessionDEK = null;
|
|
201
|
-
|
|
202
|
-
console.log('Peer disconnected, waiting for new connection...');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
disconnect(): void {
|
|
206
|
-
this.signaling.disconnect();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
getPublicKey(): string {
|
|
210
|
-
return this.config.keyPair.publicKey;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
getAgentId(): string {
|
|
214
|
-
return this.config.agentId;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generate and encode a new key pair for the agent
|
|
220
|
-
*/
|
|
221
|
-
export function generateAgentKeyPair(): { publicKey: string; secretKey: string } {
|
|
222
|
-
return encodeKeyPair(generateKeyPair());
|
|
223
|
-
}
|