@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +222 -123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -1,18 +1,16 @@
1
1
  /**
2
2
  * @dropout-ai/runtime
3
- * Universal AI Interaction Capture for Node.js & Next.js
4
- * Patches: fetch, http.request, https.request
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
- const https = require('https');
9
- const http = require('http');
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 for Auto-Detection
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
- // 🚨 BROWSER GUARD 🚨
24
- // If this runs in the browser (Client Component), do nothing.
25
- // This prevents the "listener" error and protects your API keys.
26
- if (typeof window !== 'undefined') {
27
- console.warn("[Dropout] âš ī¸ Initialization Skipped: This SDK is for Node.js/Server environments only.");
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
- // 1. Validation
29
+ // đŸ›Ąī¸ 2. SAFETY FUSE: MISSING CREDENTIALS
32
30
  if (!config.apiKey || !config.projectId) {
33
- console.warn("[Dropout] âš ī¸ Initialization Skipped: Missing apiKey or projectId.");
31
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Missing API Key or Project ID.");
34
32
  return;
35
33
  }
36
34
 
37
- // 2. Config Setup
38
- this.config = config;
39
- this.projectId = config.projectId;
40
- this.apiKey = config.apiKey;
41
- this.debug = config.debug || false;
42
- this.privacy = config.privacy || 'full';
43
- this.captureEndpoint = SUPABASE_FUNCTION_URL;
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
- // 4. Initialize Identity
53
- if (!global.__dropout_session_id__) {
54
- global.__dropout_session_id__ = this.generateSessionId();
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
- // 5. Start the Wiretap
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
- // Bind methods
61
- this.log = this.log.bind(this);
62
- this.isAiRequest = this.isAiRequest.bind(this);
63
- this.patchNetwork();
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
- if (url) {
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
- } catch (e) { }
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
- if (!this.apiKey || !this.projectId) return;
126
-
127
- const finalPayload = {
128
- ...payload,
129
- project_id: this.projectId,
130
- //content_blob: payload.content,
131
- content: payload.content,
132
- received_at: new Date().toISOString()
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
- this.log(`🚀 Sending Capture (${payload.turn_role})`);
167
+ this.log(`🚀 Sending Capture [${payload.turn_role}]`);
136
168
 
137
- const req = https.request(this.captureEndpoint, {
138
- method: 'POST',
139
- headers: {
140
- 'Content-Type': 'application/json',
141
- 'x-dropout-key': this.apiKey
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
- req.on('error', (e) => this.log("❌ Upload Failed", e.message));
146
- req.write(JSON.stringify(finalPayload));
147
- req.end();
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
- const url = typeof input === 'string' ? input : input?.url;
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: 0,
227
+ turn_index: activeTurn,
176
228
  turn_role: 'user',
177
229
  provider,
178
230
  model,
179
231
  content: _this.privacy === 'full' ? bodyStr : null,
180
- metadata_flags: { method: 'FETCH', url: url }
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
- const oText = await cloned.text();
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: 0,
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
- metadata_flags: { status: response.status }
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
- if (typeof args[0] === 'string') {
216
- url = args[0];
217
- } else {
218
- const opts = args[0] || {};
219
- const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
220
- const host = opts.hostname || opts.host || 'localhost';
221
- const path = opts.path || '/';
222
- url = `${protocol}//${host}${path}`;
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
- const chunk = writeArgs[0];
239
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
240
- reqChunks.push(Buffer.from(chunk));
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
- const chunk = endArgs[0];
247
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
248
- reqChunks.push(Buffer.from(chunk));
249
- }
250
-
251
- const reqBody = Buffer.concat(reqChunks).toString('utf8');
252
- const { provider, model } = _this.normalize(url, reqBody);
253
- clientRequest._dropout_meta = { provider, model };
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
- _this.emit({
256
- session_id: global.__dropout_session_id__,
257
- turn_index: 0,
258
- turn_role: 'user',
259
- provider,
260
- model,
261
- content: _this.privacy === 'full' ? reqBody : null,
262
- metadata_flags: { method: moduleName.toUpperCase(), url: url }
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
- const resBody = Buffer.concat(resChunks).toString('utf8');
273
- const latency = Date.now() - start;
274
- const meta = clientRequest._dropout_meta || {};
275
-
276
- _this.emit({
277
- session_id: global.__dropout_session_id__,
278
- turn_index: 0,
279
- turn_role: 'assistant',
280
- latency_ms: latency,
281
- provider: meta.provider,
282
- model: meta.model,
283
- content: _this.privacy === 'full' ? resBody : null,
284
- metadata_flags: { status: res.statusCode }
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 (Node Preload)
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,