@dropout-ai/runtime 0.3.2 → 0.3.4
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/package.json +1 -1
- package/src/index.js +197 -114
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -9,28 +9,15 @@ const https = require('https');
|
|
|
9
9
|
const http = require('http');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
|
|
12
|
-
// --- CONFIGURATION ---
|
|
12
|
+
// --- DEFAULT CONFIGURATION ---
|
|
13
|
+
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
14
|
+
|
|
15
|
+
// Known AI Domains for Auto-Detection
|
|
13
16
|
const KNOWN_AI_DOMAINS = [
|
|
14
|
-
'api.openai.com',
|
|
15
|
-
'api.
|
|
16
|
-
'generativelanguage.googleapis.com', // Gemini
|
|
17
|
-
'aiplatform.googleapis.com', // Vertex AI (Genkit often uses this)
|
|
18
|
-
'api.groq.com', // Groq
|
|
19
|
-
'api.mistral.ai', // Mistral
|
|
20
|
-
'api.cohere.ai' // Cohere
|
|
17
|
+
'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
|
|
18
|
+
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
21
19
|
];
|
|
22
20
|
|
|
23
|
-
const DROPOUT_INGEST_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
24
|
-
|
|
25
|
-
// --- UTILS ---
|
|
26
|
-
function generateSessionId() {
|
|
27
|
-
try {
|
|
28
|
-
return crypto.randomUUID();
|
|
29
|
-
} catch (e) {
|
|
30
|
-
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
class Dropout {
|
|
35
22
|
constructor(config = {}) {
|
|
36
23
|
// 1. Validation
|
|
@@ -43,10 +30,11 @@ class Dropout {
|
|
|
43
30
|
this.config = config;
|
|
44
31
|
this.projectId = config.projectId;
|
|
45
32
|
this.apiKey = config.apiKey;
|
|
46
|
-
this.debug = config.debug || false;
|
|
33
|
+
this.debug = config.debug || false;
|
|
47
34
|
this.privacy = config.privacy || 'full';
|
|
35
|
+
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
48
36
|
|
|
49
|
-
// 3. Singleton Guard
|
|
37
|
+
// 3. Singleton Guard
|
|
50
38
|
if (global.__dropout_initialized__) {
|
|
51
39
|
if (this.debug) console.log("[Dropout] ℹ️ Already initialized. Skipping patch.");
|
|
52
40
|
return;
|
|
@@ -55,118 +43,239 @@ class Dropout {
|
|
|
55
43
|
|
|
56
44
|
// 4. Initialize Identity
|
|
57
45
|
if (!global.__dropout_session_id__) {
|
|
58
|
-
global.__dropout_session_id__ = generateSessionId();
|
|
46
|
+
global.__dropout_session_id__ = this.generateSessionId();
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
// 5. Start the Wiretap
|
|
62
50
|
if (this.debug) console.log(`[Dropout] 🟢 Initialized for Project: ${this.projectId}`);
|
|
51
|
+
|
|
52
|
+
// Bind methods to 'this' to avoid scope loss
|
|
53
|
+
this.log = this.log.bind(this);
|
|
54
|
+
this.isAiRequest = this.isAiRequest.bind(this);
|
|
63
55
|
this.patchNetwork();
|
|
64
56
|
}
|
|
65
57
|
|
|
58
|
+
// --- UTILS ---
|
|
66
59
|
log(msg, ...args) {
|
|
67
60
|
if (this.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
generateSessionId() {
|
|
64
|
+
try {
|
|
65
|
+
return crypto.randomUUID();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
hash(text) {
|
|
72
|
+
if (!text) return null;
|
|
73
|
+
try {
|
|
74
|
+
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
75
|
+
} catch (e) { return 'hash_err'; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
normalize(url, body) {
|
|
79
|
+
let provider = 'custom';
|
|
80
|
+
let model = 'unknown';
|
|
81
|
+
|
|
82
|
+
if (url) {
|
|
83
|
+
const u = url.toLowerCase();
|
|
84
|
+
if (u.includes('openai')) provider = 'openai';
|
|
85
|
+
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
86
|
+
else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
|
|
87
|
+
else if (u.includes('groq')) provider = 'groq';
|
|
88
|
+
else if (u.includes('mistral')) provider = 'mistral';
|
|
89
|
+
else if (u.includes('cohere')) provider = 'cohere';
|
|
90
|
+
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (body) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
96
|
+
if (parsed.model) model = parsed.model;
|
|
97
|
+
} catch (e) { }
|
|
98
|
+
}
|
|
99
|
+
return { provider, model };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isAiRequest(url, bodyString) {
|
|
71
103
|
if (!url) return false;
|
|
72
|
-
// Guard: Infinite Loop Prevention
|
|
73
|
-
if (url.includes(
|
|
104
|
+
// Guard: Infinite Loop Prevention
|
|
105
|
+
if (url.includes(this.captureEndpoint)) return false;
|
|
106
|
+
|
|
107
|
+
// 1. Check Known Domains
|
|
108
|
+
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
109
|
+
|
|
110
|
+
// 2. Heuristic Check (for custom endpoints)
|
|
111
|
+
if (bodyString) {
|
|
112
|
+
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
113
|
+
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- EMITTER ---
|
|
119
|
+
emit(payload) {
|
|
120
|
+
// 1. Guard
|
|
121
|
+
if (!this.apiKey || !this.projectId) return;
|
|
122
|
+
|
|
123
|
+
// 2. Construct Payload
|
|
124
|
+
const finalPayload = {
|
|
125
|
+
...payload,
|
|
126
|
+
project_id: this.projectId,
|
|
127
|
+
content_blob: payload.content, // Map for new schema
|
|
128
|
+
content: payload.content, // Map for old schema (fallback)
|
|
129
|
+
received_at: new Date().toISOString()
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.log(`🚀 Sending Capture (${payload.turn_role})`);
|
|
133
|
+
|
|
134
|
+
// 3. Fire and Forget using HTTPS (Bypassing our own patches via pure request)
|
|
135
|
+
// We use a clean request to avoid triggering our own hooks if we used global.fetch
|
|
136
|
+
const req = https.request(this.captureEndpoint, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'x-dropout-key': this.apiKey
|
|
141
|
+
}
|
|
142
|
+
});
|
|
74
143
|
|
|
75
|
-
|
|
76
|
-
|
|
144
|
+
req.on('error', (e) => this.log("❌ Upload Failed", e.message));
|
|
145
|
+
req.write(JSON.stringify(finalPayload));
|
|
146
|
+
req.end();
|
|
77
147
|
}
|
|
78
148
|
|
|
79
|
-
// ---
|
|
149
|
+
// --- CORE PATCHING ---
|
|
80
150
|
patchNetwork() {
|
|
81
|
-
const _this = this;
|
|
151
|
+
const _this = this; // Capture instance
|
|
82
152
|
|
|
83
|
-
// A.
|
|
153
|
+
// --- A. PATCH FETCH ---
|
|
84
154
|
if (global.fetch) {
|
|
85
155
|
const originalFetch = global.fetch;
|
|
86
156
|
global.fetch = async function (input, init) {
|
|
87
157
|
const url = typeof input === 'string' ? input : input?.url;
|
|
88
158
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
159
|
+
// Safe Body Check
|
|
160
|
+
let bodyStr = "";
|
|
161
|
+
if (init && init.body) {
|
|
162
|
+
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
92
163
|
}
|
|
93
164
|
|
|
94
|
-
|
|
165
|
+
// Guard
|
|
166
|
+
if (!_this.isAiRequest(url, bodyStr)) {
|
|
167
|
+
return originalFetch.apply(this, arguments);
|
|
168
|
+
}
|
|
95
169
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
170
|
+
_this.log(`⚡ [FETCH] Intercepting: ${url}`);
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
|
|
173
|
+
// Emit Request
|
|
174
|
+
const { provider, model } = _this.normalize(url, bodyStr);
|
|
175
|
+
_this.emit({
|
|
176
|
+
session_id: global.__dropout_session_id__,
|
|
177
|
+
turn_index: 0,
|
|
178
|
+
turn_role: 'user',
|
|
179
|
+
provider,
|
|
180
|
+
model,
|
|
181
|
+
content: _this.privacy === 'full' ? bodyStr : null,
|
|
182
|
+
metadata_flags: { method: 'FETCH', url: url }
|
|
183
|
+
});
|
|
99
184
|
|
|
100
|
-
|
|
185
|
+
// Execute
|
|
101
186
|
let response;
|
|
102
|
-
|
|
103
|
-
// Execute Original Call
|
|
104
187
|
try {
|
|
105
188
|
response = await originalFetch.apply(this, arguments);
|
|
106
|
-
} catch (
|
|
189
|
+
} catch (err) { throw err; }
|
|
107
190
|
|
|
108
|
-
|
|
191
|
+
const latency = Date.now() - start;
|
|
192
|
+
|
|
193
|
+
// Emit Response
|
|
109
194
|
try {
|
|
110
195
|
const cloned = response.clone();
|
|
111
|
-
const
|
|
112
|
-
|
|
196
|
+
const oText = await cloned.text();
|
|
113
197
|
_this.emit({
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
198
|
+
session_id: global.__dropout_session_id__,
|
|
199
|
+
turn_index: 0,
|
|
200
|
+
turn_role: 'assistant',
|
|
201
|
+
latency_ms: latency,
|
|
202
|
+
provider,
|
|
203
|
+
model,
|
|
204
|
+
content: _this.privacy === 'full' ? oText : null,
|
|
205
|
+
metadata_flags: { status: response.status }
|
|
120
206
|
});
|
|
121
|
-
} catch (e) { _this.log("⚠️
|
|
207
|
+
} catch (e) { _this.log("⚠️ Failed to read response body"); }
|
|
122
208
|
|
|
123
209
|
return response;
|
|
124
210
|
};
|
|
125
211
|
this.log("✅ Patch Applied: global.fetch");
|
|
126
212
|
}
|
|
127
213
|
|
|
128
|
-
// B.
|
|
214
|
+
// --- B. PATCH NODE HTTP/HTTPS ---
|
|
129
215
|
const patchNodeRequest = (module, moduleName) => {
|
|
130
216
|
const originalRequest = module.request;
|
|
131
217
|
module.request = function (...args) {
|
|
132
|
-
// Resolve URL from varied arguments
|
|
133
218
|
let url;
|
|
134
|
-
|
|
219
|
+
// Argument Resolution (url, options, callback) or (options, callback)
|
|
135
220
|
if (typeof args[0] === 'string') {
|
|
136
221
|
url = args[0];
|
|
137
|
-
options = args[1] || {};
|
|
138
222
|
} else {
|
|
139
|
-
|
|
140
|
-
const protocol =
|
|
141
|
-
const host =
|
|
142
|
-
const path =
|
|
223
|
+
const opts = args[0] || {};
|
|
224
|
+
const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
|
|
225
|
+
const host = opts.hostname || opts.host || 'localhost';
|
|
226
|
+
const path = opts.path || '/';
|
|
143
227
|
url = `${protocol}//${host}${path}`;
|
|
144
228
|
}
|
|
145
229
|
|
|
146
|
-
|
|
147
|
-
if (!_this.isAiRequest(url)) {
|
|
230
|
+
if (!_this.isAiRequest(url, null)) {
|
|
148
231
|
return originalRequest.apply(this, args);
|
|
149
232
|
}
|
|
150
233
|
|
|
151
|
-
_this.log(`⚡ [${moduleName.toUpperCase()}] Intercepting
|
|
234
|
+
_this.log(`⚡ [${moduleName.toUpperCase()}] Intercepting: ${url}`);
|
|
235
|
+
const start = Date.now();
|
|
152
236
|
|
|
153
|
-
const startTime = Date.now();
|
|
154
237
|
const clientRequest = originalRequest.apply(this, args);
|
|
155
238
|
|
|
156
|
-
// Capture
|
|
157
|
-
const
|
|
239
|
+
// Capture Buffers
|
|
240
|
+
const reqChunks = [];
|
|
158
241
|
const originalWrite = clientRequest.write;
|
|
159
242
|
const originalEnd = clientRequest.end;
|
|
160
243
|
|
|
244
|
+
// SAFE WRITE PATCH
|
|
161
245
|
clientRequest.write = function (...writeArgs) {
|
|
162
|
-
|
|
246
|
+
const chunk = writeArgs[0];
|
|
247
|
+
// Only buffer if it's data (String or Buffer), ignore objects/callbacks
|
|
248
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
249
|
+
reqChunks.push(Buffer.from(chunk));
|
|
250
|
+
}
|
|
163
251
|
return originalWrite.apply(this, writeArgs);
|
|
164
252
|
};
|
|
165
253
|
|
|
254
|
+
// SAFE END PATCH
|
|
166
255
|
clientRequest.end = function (...endArgs) {
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
256
|
+
const chunk = endArgs[0];
|
|
257
|
+
// Only buffer if it's data
|
|
258
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
259
|
+
reqChunks.push(Buffer.from(chunk));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Emit Request
|
|
263
|
+
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
264
|
+
const { provider, model } = _this.normalize(url, reqBody);
|
|
265
|
+
|
|
266
|
+
// Attach state to request for response handler
|
|
267
|
+
clientRequest._dropout_meta = { provider, model };
|
|
268
|
+
|
|
269
|
+
_this.emit({
|
|
270
|
+
session_id: global.__dropout_session_id__,
|
|
271
|
+
turn_index: 0,
|
|
272
|
+
turn_role: 'user',
|
|
273
|
+
provider,
|
|
274
|
+
model,
|
|
275
|
+
content: _this.privacy === 'full' ? reqBody : null,
|
|
276
|
+
metadata_flags: { method: moduleName.toUpperCase(), url: url }
|
|
277
|
+
});
|
|
278
|
+
|
|
170
279
|
return originalEnd.apply(this, endArgs);
|
|
171
280
|
};
|
|
172
281
|
|
|
@@ -176,13 +285,18 @@ class Dropout {
|
|
|
176
285
|
res.on('data', (chunk) => resChunks.push(chunk));
|
|
177
286
|
res.on('end', () => {
|
|
178
287
|
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
288
|
+
const latency = Date.now() - start;
|
|
289
|
+
const meta = clientRequest._dropout_meta || {};
|
|
290
|
+
|
|
179
291
|
_this.emit({
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
292
|
+
session_id: global.__dropout_session_id__,
|
|
293
|
+
turn_index: 0,
|
|
294
|
+
turn_role: 'assistant',
|
|
295
|
+
latency_ms: latency,
|
|
296
|
+
provider: meta.provider,
|
|
297
|
+
model: meta.model,
|
|
298
|
+
content: _this.privacy === 'full' ? resBody : null,
|
|
299
|
+
metadata_flags: { status: res.statusCode }
|
|
186
300
|
});
|
|
187
301
|
});
|
|
188
302
|
});
|
|
@@ -195,46 +309,15 @@ class Dropout {
|
|
|
195
309
|
patchNodeRequest(http, 'http');
|
|
196
310
|
this.log("✅ Patch Applied: http/https");
|
|
197
311
|
}
|
|
312
|
+
}
|
|
198
313
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
project_id: this.projectId,
|
|
207
|
-
session_id: global.__dropout_session_id__,
|
|
208
|
-
turn_role: 'assistant', // Default to assistant for system-captured logs
|
|
209
|
-
turn_index: 0, // In universal mode, we capture raw streams
|
|
210
|
-
//content_blob: this.privacy === 'full' ? content : null,
|
|
211
|
-
content: this.privacy === 'full' ? content : null,
|
|
212
|
-
metadata_flags: {
|
|
213
|
-
latency: data.latency,
|
|
214
|
-
url: data.url,
|
|
215
|
-
method: data.method,
|
|
216
|
-
status: data.status,
|
|
217
|
-
captured_via: 'universal_interceptor'
|
|
218
|
-
},
|
|
219
|
-
received_at: new Date().toISOString()
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
this.log(`🚀 Sending Capture to Supabase (${data.latency}ms)`);
|
|
223
|
-
|
|
224
|
-
// 2. Fire and Forget (Using unpatched fetch mechanism)
|
|
225
|
-
// We use a clean http request to avoid triggering our own hooks if we used global.fetch
|
|
226
|
-
const req = https.request(DROPOUT_INGEST_URL, {
|
|
227
|
-
method: 'POST',
|
|
228
|
-
headers: {
|
|
229
|
-
'Content-Type': 'application/json',
|
|
230
|
-
'x-dropout-key': this.apiKey
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
req.on('error', (e) => this.log("❌ Upload Failed", e.message));
|
|
235
|
-
req.write(JSON.stringify(payload));
|
|
236
|
-
req.end();
|
|
237
|
-
}
|
|
314
|
+
// Auto-Start (Node Preload)
|
|
315
|
+
if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
|
|
316
|
+
new Dropout({
|
|
317
|
+
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
318
|
+
apiKey: process.env.DROPOUT_API_KEY,
|
|
319
|
+
debug: process.env.DROPOUT_DEBUG === 'true'
|
|
320
|
+
});
|
|
238
321
|
}
|
|
239
322
|
|
|
240
323
|
module.exports = Dropout;
|