@dropout-ai/runtime 0.3.5 â 0.3.7
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 +222 -123
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
|
-
* Universal AI Interaction Capture for Node.js
|
|
4
|
-
*
|
|
5
|
-
* Capability: Captures Genkit, OpenAI, LangChain, Axios, and standard fetch.
|
|
3
|
+
* Universal AI Interaction Capture for Node.js
|
|
4
|
+
* Stability: High (Browser-Safe, Silent Failures)
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const crypto = require('crypto');
|
|
7
|
+
// We define these at the top scope but require them lazily or safely
|
|
8
|
+
let https, http, crypto;
|
|
11
9
|
|
|
12
10
|
// --- DEFAULT CONFIGURATION ---
|
|
13
11
|
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
14
12
|
|
|
15
|
-
// Known AI Domains
|
|
13
|
+
// Known AI Domains
|
|
16
14
|
const KNOWN_AI_DOMAINS = [
|
|
17
15
|
'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
|
|
18
16
|
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
@@ -20,47 +18,73 @@ const KNOWN_AI_DOMAINS = [
|
|
|
20
18
|
|
|
21
19
|
class Dropout {
|
|
22
20
|
constructor(config = {}) {
|
|
23
|
-
//
|
|
24
|
-
// If this runs in
|
|
25
|
-
// This prevents the "listener" error and protects your
|
|
26
|
-
if (typeof window !== 'undefined') {
|
|
27
|
-
console.warn("[Dropout] â ī¸
|
|
21
|
+
// đĄī¸ 1. SAFETY FUSE: BROWSER CHECK
|
|
22
|
+
// If this runs in a browser (Client Component), ABORT IMMEDIATELY.
|
|
23
|
+
// This prevents the "listener" error and protects your app from crashing.
|
|
24
|
+
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
|
|
25
|
+
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: SDK detected browser environment. Running in Server-Mode only.");
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
//
|
|
29
|
+
// đĄī¸ 2. SAFETY FUSE: MISSING CREDENTIALS
|
|
32
30
|
if (!config.apiKey || !config.projectId) {
|
|
33
|
-
console.warn("[Dropout] â ī¸
|
|
31
|
+
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: Missing API Key or Project ID.");
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// 3. Singleton Guard
|
|
46
|
-
if (global.__dropout_initialized__) {
|
|
47
|
-
if (this.debug) console.log("[Dropout] âšī¸ Already initialized. Skipping patch.");
|
|
35
|
+
// đĄī¸ 3. SAFETY FUSE: DEPENDENCY CHECK
|
|
36
|
+
// We try to load Node modules. If this fails (rare runtime edge case), we abort.
|
|
37
|
+
try {
|
|
38
|
+
https = require('https');
|
|
39
|
+
http = require('http');
|
|
40
|
+
crypto = require('crypto');
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn("[Dropout] â ī¸ Skipped: Node.js core modules missing.");
|
|
48
43
|
return;
|
|
49
44
|
}
|
|
50
|
-
global.__dropout_initialized__ = true;
|
|
51
45
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
// --- INITIALIZATION ---
|
|
47
|
+
try {
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.projectId = config.projectId;
|
|
50
|
+
this.apiKey = config.apiKey;
|
|
51
|
+
this.debug = config.debug || false;
|
|
52
|
+
this.privacy = config.privacy || 'full';
|
|
53
|
+
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
54
|
+
this.maxOutputBytes = 32768;
|
|
55
|
+
|
|
56
|
+
// Conversation State
|
|
57
|
+
this.turnIndex = 0;
|
|
58
|
+
this.lastTurnConfirmed = true;
|
|
59
|
+
this.lastPromptHash = null;
|
|
60
|
+
this.lastResponseHash = null;
|
|
61
|
+
|
|
62
|
+
// Singleton Guard
|
|
63
|
+
if (global.__dropout_initialized__) {
|
|
64
|
+
if (this.debug) console.log("[Dropout] âšī¸ Already initialized.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
global.__dropout_initialized__ = true;
|
|
68
|
+
|
|
69
|
+
// Identity
|
|
70
|
+
if (!global.__dropout_session_id__) {
|
|
71
|
+
global.__dropout_session_id__ = this.generateSessionId();
|
|
72
|
+
}
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
if (this.debug) console.log(`[Dropout] đĸ Initialized for Project: ${this.projectId}`);
|
|
74
|
+
if (this.debug) console.log(`[Dropout] đĸ Online: ${this.projectId}`);
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
// Bindings
|
|
77
|
+
this.log = this.log.bind(this);
|
|
78
|
+
this.emit = this.emit.bind(this);
|
|
79
|
+
|
|
80
|
+
// đĄī¸ 4. SAFETY FUSE: NETWORK PATCHING
|
|
81
|
+
// If patching fails, we log it and continue. We DO NOT crash the app.
|
|
82
|
+
this.patchNetwork();
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// THE ULTIMATE CATCH-ALL
|
|
86
|
+
console.error("[Dropout] â Critical Initialization Error (App will continue):", err);
|
|
87
|
+
}
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
// --- UTILS ---
|
|
@@ -86,24 +110,24 @@ class Dropout {
|
|
|
86
110
|
normalize(url, body) {
|
|
87
111
|
let provider = 'custom';
|
|
88
112
|
let model = 'unknown';
|
|
113
|
+
try {
|
|
114
|
+
if (url) {
|
|
115
|
+
const u = url.toLowerCase();
|
|
116
|
+
if (u.includes('openai')) provider = 'openai';
|
|
117
|
+
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
118
|
+
else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
|
|
119
|
+
else if (u.includes('groq')) provider = 'groq';
|
|
120
|
+
else if (u.includes('mistral')) provider = 'mistral';
|
|
121
|
+
else if (u.includes('cohere')) provider = 'cohere';
|
|
122
|
+
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
123
|
+
}
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
const u = url.toLowerCase();
|
|
92
|
-
if (u.includes('openai')) provider = 'openai';
|
|
93
|
-
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
94
|
-
else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
|
|
95
|
-
else if (u.includes('groq')) provider = 'groq';
|
|
96
|
-
else if (u.includes('mistral')) provider = 'mistral';
|
|
97
|
-
else if (u.includes('cohere')) provider = 'cohere';
|
|
98
|
-
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (body) {
|
|
102
|
-
try {
|
|
125
|
+
if (body) {
|
|
103
126
|
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
104
127
|
if (parsed.model) model = parsed.model;
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
|
|
129
|
+
}
|
|
130
|
+
} catch (e) { /* Ignore parsing errors safely */ }
|
|
107
131
|
return { provider, model };
|
|
108
132
|
}
|
|
109
133
|
|
|
@@ -122,29 +146,41 @@ class Dropout {
|
|
|
122
146
|
|
|
123
147
|
// --- EMITTER ---
|
|
124
148
|
emit(payload) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
try {
|
|
150
|
+
if (!this.apiKey || !this.projectId) return;
|
|
151
|
+
|
|
152
|
+
const finalPayload = {
|
|
153
|
+
project_id: this.projectId,
|
|
154
|
+
session_id: payload.session_id,
|
|
155
|
+
turn_index: payload.turn_index,
|
|
156
|
+
turn_role: payload.turn_role,
|
|
157
|
+
provider: payload.provider,
|
|
158
|
+
model: payload.model,
|
|
159
|
+
latency_ms: payload.latency_ms || null,
|
|
160
|
+
content: payload.content,
|
|
161
|
+
//content_blob: payload.content,
|
|
162
|
+
content_hash: payload.content_hash,
|
|
163
|
+
metadata_flags: payload.metadata_flags,
|
|
164
|
+
received_at: new Date().toISOString()
|
|
165
|
+
};
|
|
134
166
|
|
|
135
|
-
|
|
167
|
+
this.log(`đ Sending Capture [${payload.turn_role}]`);
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
// Use pure Node request to bypass patches
|
|
170
|
+
const req = https.request(this.captureEndpoint, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'x-dropout-key': this.apiKey
|
|
175
|
+
}
|
|
176
|
+
});
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
req.on('error', (e) => this.log("â Upload Failed (Silent)", e.message));
|
|
179
|
+
req.write(JSON.stringify(finalPayload));
|
|
180
|
+
req.end();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
this.log("â ī¸ Emit Error (Silent)", e);
|
|
183
|
+
}
|
|
148
184
|
}
|
|
149
185
|
|
|
150
186
|
// --- CORE PATCHING ---
|
|
@@ -155,13 +191,17 @@ class Dropout {
|
|
|
155
191
|
if (global.fetch) {
|
|
156
192
|
const originalFetch = global.fetch;
|
|
157
193
|
global.fetch = async function (input, init) {
|
|
158
|
-
|
|
194
|
+
// 1. Safe URL Extraction
|
|
195
|
+
let url;
|
|
196
|
+
try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
|
|
159
197
|
|
|
198
|
+
// 2. Safe Body Extraction
|
|
160
199
|
let bodyStr = "";
|
|
161
200
|
if (init && init.body) {
|
|
162
201
|
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
163
202
|
}
|
|
164
203
|
|
|
204
|
+
// 3. Guard
|
|
165
205
|
if (!_this.isAiRequest(url, bodyStr)) {
|
|
166
206
|
return originalFetch.apply(this, arguments);
|
|
167
207
|
}
|
|
@@ -169,17 +209,32 @@ class Dropout {
|
|
|
169
209
|
_this.log(`⥠[FETCH] Intercepting: ${url}`);
|
|
170
210
|
const start = Date.now();
|
|
171
211
|
|
|
212
|
+
// 4. Determine Turn
|
|
213
|
+
let activeTurn;
|
|
214
|
+
if (_this.lastTurnConfirmed) {
|
|
215
|
+
activeTurn = _this.turnIndex++;
|
|
216
|
+
_this.lastTurnConfirmed = false;
|
|
217
|
+
} else {
|
|
218
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
172
221
|
const { provider, model } = _this.normalize(url, bodyStr);
|
|
222
|
+
const pHash = _this.hash(bodyStr);
|
|
223
|
+
|
|
224
|
+
// 5. Emit User Request
|
|
173
225
|
_this.emit({
|
|
174
226
|
session_id: global.__dropout_session_id__,
|
|
175
|
-
turn_index:
|
|
227
|
+
turn_index: activeTurn,
|
|
176
228
|
turn_role: 'user',
|
|
177
229
|
provider,
|
|
178
230
|
model,
|
|
179
231
|
content: _this.privacy === 'full' ? bodyStr : null,
|
|
180
|
-
|
|
232
|
+
content_hash: pHash,
|
|
233
|
+
metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: 'FETCH', url: url }
|
|
181
234
|
});
|
|
235
|
+
_this.lastPromptHash = pHash;
|
|
182
236
|
|
|
237
|
+
// 6. Execute Original (Wrapped)
|
|
183
238
|
let response;
|
|
184
239
|
try {
|
|
185
240
|
response = await originalFetch.apply(this, arguments);
|
|
@@ -187,19 +242,32 @@ class Dropout {
|
|
|
187
242
|
|
|
188
243
|
const latency = Date.now() - start;
|
|
189
244
|
|
|
245
|
+
// 7. Emit AI Response (Non-Blocking)
|
|
190
246
|
try {
|
|
191
247
|
const cloned = response.clone();
|
|
192
|
-
|
|
248
|
+
let oText = await cloned.text();
|
|
249
|
+
if (oText && oText.length > _this.maxOutputBytes) {
|
|
250
|
+
oText = oText.slice(0, _this.maxOutputBytes);
|
|
251
|
+
}
|
|
252
|
+
const oHash = _this.hash(oText);
|
|
253
|
+
|
|
193
254
|
_this.emit({
|
|
194
255
|
session_id: global.__dropout_session_id__,
|
|
195
|
-
turn_index:
|
|
256
|
+
turn_index: activeTurn,
|
|
196
257
|
turn_role: 'assistant',
|
|
197
258
|
latency_ms: latency,
|
|
198
259
|
provider,
|
|
199
260
|
model,
|
|
200
261
|
content: _this.privacy === 'full' ? oText : null,
|
|
201
|
-
|
|
262
|
+
content_hash: oHash,
|
|
263
|
+
metadata_flags: {
|
|
264
|
+
non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
|
|
265
|
+
turn_boundary_confirmed: 1,
|
|
266
|
+
status: response.status
|
|
267
|
+
}
|
|
202
268
|
});
|
|
269
|
+
_this.lastResponseHash = oHash;
|
|
270
|
+
_this.lastTurnConfirmed = true;
|
|
203
271
|
} catch (e) { _this.log("â ī¸ Failed to read response body"); }
|
|
204
272
|
|
|
205
273
|
return response;
|
|
@@ -212,15 +280,17 @@ class Dropout {
|
|
|
212
280
|
const originalRequest = module.request;
|
|
213
281
|
module.request = function (...args) {
|
|
214
282
|
let url;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
283
|
+
try {
|
|
284
|
+
if (typeof args[0] === 'string') {
|
|
285
|
+
url = args[0];
|
|
286
|
+
} else {
|
|
287
|
+
const opts = args[0] || {};
|
|
288
|
+
const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
|
|
289
|
+
const host = opts.hostname || opts.host || 'localhost';
|
|
290
|
+
const path = opts.path || '/';
|
|
291
|
+
url = `${protocol}//${host}${path}`;
|
|
292
|
+
}
|
|
293
|
+
} catch (e) { return originalRequest.apply(this, args); }
|
|
224
294
|
|
|
225
295
|
if (!_this.isAiRequest(url, null)) {
|
|
226
296
|
return originalRequest.apply(this, args);
|
|
@@ -234,55 +304,84 @@ class Dropout {
|
|
|
234
304
|
const originalWrite = clientRequest.write;
|
|
235
305
|
const originalEnd = clientRequest.end;
|
|
236
306
|
|
|
307
|
+
// SAFE WRITE PATCH
|
|
237
308
|
clientRequest.write = function (...writeArgs) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
309
|
+
try {
|
|
310
|
+
const chunk = writeArgs[0];
|
|
311
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
312
|
+
reqChunks.push(Buffer.from(chunk));
|
|
313
|
+
}
|
|
314
|
+
} catch (e) { }
|
|
242
315
|
return originalWrite.apply(this, writeArgs);
|
|
243
316
|
};
|
|
244
317
|
|
|
318
|
+
// SAFE END PATCH
|
|
245
319
|
clientRequest.end = function (...endArgs) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
320
|
+
try {
|
|
321
|
+
const chunk = endArgs[0];
|
|
322
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
323
|
+
reqChunks.push(Buffer.from(chunk));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
327
|
+
const { provider, model } = _this.normalize(url, reqBody);
|
|
328
|
+
const pHash = _this.hash(reqBody);
|
|
329
|
+
|
|
330
|
+
let activeTurn;
|
|
331
|
+
if (_this.lastTurnConfirmed) {
|
|
332
|
+
activeTurn = _this.turnIndex++;
|
|
333
|
+
_this.lastTurnConfirmed = false;
|
|
334
|
+
} else {
|
|
335
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
clientRequest._dropout_turn = activeTurn;
|
|
339
|
+
clientRequest._dropout_provider = provider;
|
|
340
|
+
clientRequest._dropout_model = model;
|
|
254
341
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
342
|
+
_this.emit({
|
|
343
|
+
session_id: global.__dropout_session_id__,
|
|
344
|
+
turn_index: activeTurn,
|
|
345
|
+
turn_role: 'user',
|
|
346
|
+
provider,
|
|
347
|
+
model,
|
|
348
|
+
content: _this.privacy === 'full' ? reqBody : null,
|
|
349
|
+
content_hash: pHash,
|
|
350
|
+
metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: moduleName.toUpperCase(), url: url }
|
|
351
|
+
});
|
|
352
|
+
_this.lastPromptHash = pHash;
|
|
353
|
+
} catch (e) { _this.log("â ī¸ Request Capture Failed"); }
|
|
264
354
|
|
|
265
355
|
return originalEnd.apply(this, endArgs);
|
|
266
356
|
};
|
|
267
357
|
|
|
358
|
+
// SAFE RESPONSE LISTENER
|
|
268
359
|
clientRequest.on('response', (res) => {
|
|
269
360
|
const resChunks = [];
|
|
270
361
|
res.on('data', (chunk) => resChunks.push(chunk));
|
|
271
362
|
res.on('end', () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
363
|
+
try {
|
|
364
|
+
let resBody = Buffer.concat(resChunks).toString('utf8');
|
|
365
|
+
if (resBody && resBody.length > _this.maxOutputBytes) resBody = resBody.slice(0, _this.maxOutputBytes);
|
|
366
|
+
|
|
367
|
+
const latency = Date.now() - start;
|
|
368
|
+
const oHash = _this.hash(resBody);
|
|
369
|
+
|
|
370
|
+
_this.emit({
|
|
371
|
+
session_id: global.__dropout_session_id__,
|
|
372
|
+
turn_index: clientRequest._dropout_turn || 0,
|
|
373
|
+
turn_role: 'assistant',
|
|
374
|
+
latency_ms: latency,
|
|
375
|
+
provider: clientRequest._dropout_provider,
|
|
376
|
+
model: clientRequest._dropout_model,
|
|
377
|
+
content: _this.privacy === 'full' ? resBody : null,
|
|
378
|
+
content_hash: oHash,
|
|
379
|
+
metadata_flags: { status: res.statusCode, non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0 }
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
_this.lastResponseHash = oHash;
|
|
383
|
+
_this.lastTurnConfirmed = true;
|
|
384
|
+
} catch (e) { _this.log("â ī¸ Response Capture Failed"); }
|
|
286
385
|
});
|
|
287
386
|
});
|
|
288
387
|
|
|
@@ -296,8 +395,8 @@ class Dropout {
|
|
|
296
395
|
}
|
|
297
396
|
}
|
|
298
397
|
|
|
299
|
-
// Auto-Start (
|
|
300
|
-
if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
|
|
398
|
+
// Auto-Start (Server-Side Only)
|
|
399
|
+
if (typeof process !== 'undefined' && process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
|
|
301
400
|
new Dropout({
|
|
302
401
|
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
303
402
|
apiKey: process.env.DROPOUT_API_KEY,
|