@dropout-ai/runtime 0.3.6 â 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 +188 -176
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Universal AI Interaction Capture for Node.js
|
|
4
|
-
*
|
|
5
|
-
* Behavior: Splits interactions into separate User/Assistant rows for full analytics.
|
|
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";
|
|
@@ -20,52 +18,73 @@ const KNOWN_AI_DOMAINS = [
|
|
|
20
18
|
|
|
21
19
|
class Dropout {
|
|
22
20
|
constructor(config = {}) {
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
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.");
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// đĄī¸ 2. SAFETY FUSE: MISSING CREDENTIALS
|
|
30
30
|
if (!config.apiKey || !config.projectId) {
|
|
31
|
-
console.warn("[Dropout] â ī¸
|
|
31
|
+
if (config.debug) console.warn("[Dropout] â ī¸ Skipped: Missing API Key or Project ID.");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// 3. Conversation State (Matches your snippet)
|
|
45
|
-
this.turnIndex = 0;
|
|
46
|
-
this.lastTurnConfirmed = true;
|
|
47
|
-
this.lastPromptHash = null;
|
|
48
|
-
this.lastResponseHash = null;
|
|
49
|
-
|
|
50
|
-
// 4. Singleton Guard
|
|
51
|
-
if (global.__dropout_initialized__) {
|
|
52
|
-
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.");
|
|
53
43
|
return;
|
|
54
44
|
}
|
|
55
|
-
global.__dropout_initialized__ = true;
|
|
56
45
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
if (this.debug) console.log(`[Dropout] đĸ Initialized for Project: ${this.projectId}`);
|
|
74
|
+
if (this.debug) console.log(`[Dropout] đĸ Online: ${this.projectId}`);
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
// --- UTILS ---
|
|
@@ -91,26 +110,24 @@ class Dropout {
|
|
|
91
110
|
normalize(url, body) {
|
|
92
111
|
let provider = 'custom';
|
|
93
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
|
+
}
|
|
94
124
|
|
|
95
|
-
|
|
96
|
-
const u = url.toLowerCase();
|
|
97
|
-
if (u.includes('openai')) provider = 'openai';
|
|
98
|
-
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
99
|
-
else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
|
|
100
|
-
else if (u.includes('groq')) provider = 'groq';
|
|
101
|
-
else if (u.includes('mistral')) provider = 'mistral';
|
|
102
|
-
else if (u.includes('cohere')) provider = 'cohere';
|
|
103
|
-
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (body) {
|
|
107
|
-
try {
|
|
125
|
+
if (body) {
|
|
108
126
|
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
109
127
|
if (parsed.model) model = parsed.model;
|
|
110
|
-
// Simple heuristic fallback if model is missing but structure looks AI-ish
|
|
111
128
|
if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
|
|
112
|
-
}
|
|
113
|
-
}
|
|
129
|
+
}
|
|
130
|
+
} catch (e) { /* Ignore parsing errors safely */ }
|
|
114
131
|
return { provider, model };
|
|
115
132
|
}
|
|
116
133
|
|
|
@@ -129,43 +146,41 @@ class Dropout {
|
|
|
129
146
|
|
|
130
147
|
// --- EMITTER ---
|
|
131
148
|
emit(payload) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
content_hash: payload.content_hash,
|
|
150
|
-
|
|
151
|
-
// Metadata
|
|
152
|
-
metadata_flags: payload.metadata_flags,
|
|
153
|
-
received_at: new Date().toISOString()
|
|
154
|
-
};
|
|
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
|
+
};
|
|
155
166
|
|
|
156
|
-
|
|
167
|
+
this.log(`đ Sending Capture [${payload.turn_role}]`);
|
|
157
168
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
});
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
// --- CORE PATCHING ---
|
|
@@ -176,14 +191,17 @@ class Dropout {
|
|
|
176
191
|
if (global.fetch) {
|
|
177
192
|
const originalFetch = global.fetch;
|
|
178
193
|
global.fetch = async function (input, init) {
|
|
179
|
-
|
|
194
|
+
// 1. Safe URL Extraction
|
|
195
|
+
let url;
|
|
196
|
+
try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
|
|
180
197
|
|
|
198
|
+
// 2. Safe Body Extraction
|
|
181
199
|
let bodyStr = "";
|
|
182
200
|
if (init && init.body) {
|
|
183
201
|
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
184
202
|
}
|
|
185
203
|
|
|
186
|
-
//
|
|
204
|
+
// 3. Guard
|
|
187
205
|
if (!_this.isAiRequest(url, bodyStr)) {
|
|
188
206
|
return originalFetch.apply(this, arguments);
|
|
189
207
|
}
|
|
@@ -191,7 +209,7 @@ class Dropout {
|
|
|
191
209
|
_this.log(`⥠[FETCH] Intercepting: ${url}`);
|
|
192
210
|
const start = Date.now();
|
|
193
211
|
|
|
194
|
-
//
|
|
212
|
+
// 4. Determine Turn
|
|
195
213
|
let activeTurn;
|
|
196
214
|
if (_this.lastTurnConfirmed) {
|
|
197
215
|
activeTurn = _this.turnIndex++;
|
|
@@ -203,7 +221,7 @@ class Dropout {
|
|
|
203
221
|
const { provider, model } = _this.normalize(url, bodyStr);
|
|
204
222
|
const pHash = _this.hash(bodyStr);
|
|
205
223
|
|
|
206
|
-
//
|
|
224
|
+
// 5. Emit User Request
|
|
207
225
|
_this.emit({
|
|
208
226
|
session_id: global.__dropout_session_id__,
|
|
209
227
|
turn_index: activeTurn,
|
|
@@ -212,15 +230,11 @@ class Dropout {
|
|
|
212
230
|
model,
|
|
213
231
|
content: _this.privacy === 'full' ? bodyStr : null,
|
|
214
232
|
content_hash: pHash,
|
|
215
|
-
metadata_flags: {
|
|
216
|
-
retry_like: pHash === _this.lastPromptHash ? 1 : 0,
|
|
217
|
-
method: 'FETCH',
|
|
218
|
-
url: url
|
|
219
|
-
}
|
|
233
|
+
metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: 'FETCH', url: url }
|
|
220
234
|
});
|
|
221
235
|
_this.lastPromptHash = pHash;
|
|
222
236
|
|
|
223
|
-
//
|
|
237
|
+
// 6. Execute Original (Wrapped)
|
|
224
238
|
let response;
|
|
225
239
|
try {
|
|
226
240
|
response = await originalFetch.apply(this, arguments);
|
|
@@ -228,7 +242,7 @@ class Dropout {
|
|
|
228
242
|
|
|
229
243
|
const latency = Date.now() - start;
|
|
230
244
|
|
|
231
|
-
//
|
|
245
|
+
// 7. Emit AI Response (Non-Blocking)
|
|
232
246
|
try {
|
|
233
247
|
const cloned = response.clone();
|
|
234
248
|
let oText = await cloned.text();
|
|
@@ -266,15 +280,17 @@ class Dropout {
|
|
|
266
280
|
const originalRequest = module.request;
|
|
267
281
|
module.request = function (...args) {
|
|
268
282
|
let url;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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); }
|
|
278
294
|
|
|
279
295
|
if (!_this.isAiRequest(url, null)) {
|
|
280
296
|
return originalRequest.apply(this, args);
|
|
@@ -288,88 +304,84 @@ class Dropout {
|
|
|
288
304
|
const originalWrite = clientRequest.write;
|
|
289
305
|
const originalEnd = clientRequest.end;
|
|
290
306
|
|
|
291
|
-
//
|
|
307
|
+
// SAFE WRITE PATCH
|
|
292
308
|
clientRequest.write = function (...writeArgs) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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) { }
|
|
297
315
|
return originalWrite.apply(this, writeArgs);
|
|
298
316
|
};
|
|
299
317
|
|
|
318
|
+
// SAFE END PATCH
|
|
300
319
|
clientRequest.end = function (...endArgs) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
320
|
+
try {
|
|
321
|
+
const chunk = endArgs[0];
|
|
322
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
323
|
+
reqChunks.push(Buffer.from(chunk));
|
|
324
|
+
}
|
|
305
325
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const pHash = _this.hash(reqBody);
|
|
326
|
+
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
327
|
+
const { provider, model } = _this.normalize(url, reqBody);
|
|
328
|
+
const pHash = _this.hash(reqBody);
|
|
310
329
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
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
|
+
}
|
|
319
337
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
clientRequest._dropout_model = model;
|
|
338
|
+
clientRequest._dropout_turn = activeTurn;
|
|
339
|
+
clientRequest._dropout_provider = provider;
|
|
340
|
+
clientRequest._dropout_model = model;
|
|
324
341
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
_this.lastPromptHash = pHash;
|
|
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"); }
|
|
340
354
|
|
|
341
355
|
return originalEnd.apply(this, endArgs);
|
|
342
356
|
};
|
|
343
357
|
|
|
344
|
-
//
|
|
358
|
+
// SAFE RESPONSE LISTENER
|
|
345
359
|
clientRequest.on('response', (res) => {
|
|
346
360
|
const resChunks = [];
|
|
347
361
|
res.on('data', (chunk) => resChunks.push(chunk));
|
|
348
362
|
res.on('end', () => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
resBody = resBody.slice(0, _this.maxOutputBytes);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
_this.lastResponseHash = oHash;
|
|
372
|
-
_this.lastTurnConfirmed = true;
|
|
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"); }
|
|
373
385
|
});
|
|
374
386
|
});
|
|
375
387
|
|
|
@@ -383,8 +395,8 @@ class Dropout {
|
|
|
383
395
|
}
|
|
384
396
|
}
|
|
385
397
|
|
|
386
|
-
// Auto-Start (
|
|
387
|
-
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) {
|
|
388
400
|
new Dropout({
|
|
389
401
|
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
390
402
|
apiKey: process.env.DROPOUT_API_KEY,
|