@dropout-ai/runtime 0.5.0 → 0.5.2

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 +187 -329
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Invisible Node.js runtime for understanding behavioral impact of AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.js CHANGED
@@ -1,441 +1,299 @@
1
1
  /**
2
2
  * @dropout-ai/runtime
3
3
  * Universal AI Interaction Capture for Node.js
4
- * Stability: High (Browser-Safe, Silent Failures)
5
- * Features: Auto-Discovery, Connectivity Check, Dual-Schema Support
4
+ * Stability: High (Browser-Safe, Next.js Safe)
6
5
  */
7
6
 
8
7
  let https, http, crypto;
9
8
 
10
9
  // --- DEFAULT CONFIGURATION ---
11
- const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
10
+ const SUPABASE_FUNCTION_URL =
11
+ "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
12
12
 
13
13
  // Known AI Domains
14
14
  const KNOWN_AI_DOMAINS = [
15
- 'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
16
- 'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
15
+ "api.openai.com",
16
+ "api.anthropic.com",
17
+ "generativelanguage.googleapis.com",
18
+ "aiplatform.googleapis.com",
19
+ "api.groq.com",
20
+ "api.mistral.ai",
21
+ "api.cohere.ai",
17
22
  ];
18
23
 
19
24
  class Dropout {
20
- // Singleton Instance Storage
21
25
  static instance = null;
22
26
 
23
- /**
24
- * Universal Initialization Method (Idempotent)
25
- * Users call this explicitly in their code.
26
- */
27
27
  static init(config) {
28
- if (!Dropout.instance) {
29
- new Dropout(config);
30
- }
28
+ if (!Dropout.instance) new Dropout(config);
31
29
  return Dropout.instance;
32
30
  }
33
31
 
34
32
  constructor(config = {}) {
35
- // đŸ›Ąī¸ SINGLETON GUARD (Prevent multiple initializations)
36
- if (Dropout.instance) {
37
- if (config.debug) console.log("[Dropout] â„šī¸ Already initialized. Returning existing instance.");
38
- return Dropout.instance;
39
- }
33
+ if (Dropout.instance) return Dropout.instance;
40
34
 
41
- // đŸ›Ąī¸ 1. BROWSER GUARD (Client-Side Protection)
42
- if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
43
- if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Browser detected. SDK is Server-Only.");
35
+ // đŸ›Ąī¸ Browser Guard (prevents client execution)
36
+ if (typeof window !== "undefined" && typeof window.document !== "undefined") {
44
37
  return;
45
38
  }
46
39
 
47
- // đŸ›Ąī¸ 2. CREDENTIAL GUARD
40
+ // đŸ›Ąī¸ Credential Guard
48
41
  if (!config.apiKey || !config.projectId) {
49
42
  if (config.debug) {
50
- console.warn("\x1b[31m%s\x1b[0m", "[Dropout] 🔴 Initialization Failed: Missing API Key or Project ID.");
43
+ console.warn("[Dropout] Missing API Key or Project ID");
51
44
  }
52
45
  return;
53
46
  }
54
47
 
55
- // đŸ›Ąī¸ 3. DEPENDENCY CHECK
56
48
  try {
57
- https = require('https');
58
- http = require('http');
59
- crypto = require('crypto');
60
- } catch (e) {
61
- if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Node core modules missing.");
49
+ https = require("https");
50
+ http = require("http");
51
+ crypto = require("crypto");
52
+ } catch {
62
53
  return;
63
54
  }
64
55
 
65
- // --- INITIALIZATION ---
66
- try {
67
- this.config = config;
68
- this.projectId = config.projectId;
69
- this.apiKey = config.apiKey;
70
- this.debug = config.debug || false;
71
- this.privacy = config.privacy || 'full';
72
- this.captureEndpoint = config.captureEndpoint || SUPABASE_FUNCTION_URL;
73
- this.maxOutputBytes = 32768;
74
-
75
- // State
76
- this.turnIndex = 0;
77
- this.lastTurnConfirmed = true;
78
- this.lastPromptHash = null;
79
- this.lastResponseHash = null;
80
-
81
- // Global Guard (Extra safety for HMR environments like Next.js)
82
- if (global.__dropout_initialized__) {
83
- if (this.debug) console.log("[Dropout] â„šī¸ Global flag found. Skipping re-initialization.");
84
- Dropout.instance = this; // Re-bind if lost
85
- return;
86
- }
87
- global.__dropout_initialized__ = true;
88
-
89
- // ✅ Session Identity (Instance-Level, Not Global)
90
- // Accept session_id from config OR generate a default
91
- // Host app controls session lifecycle via startNewSession()
92
- this.currentSessionId = config.sessionId || this.generateSessionId();
93
-
94
- // ✅ VISUAL SUCCESS INDICATOR
95
- if (this.debug) {
96
- console.log('\x1b[32m%s\x1b[0m', `[Dropout] đŸŸĸ Online | Project: ${this.projectId}`);
97
- }
56
+ this.projectId = config.projectId;
57
+ this.apiKey = config.apiKey;
58
+ this.debug = config.debug || false;
59
+ this.privacy = config.privacy || "full";
60
+ this.captureEndpoint = config.captureEndpoint || SUPABASE_FUNCTION_URL;
61
+ this.maxOutputBytes = 32768;
98
62
 
99
- // Bindings
100
- this.log = this.log.bind(this);
101
- this.emit = this.emit.bind(this);
63
+ // --------------------------------------------------
64
+ // ✅ ASYNC CONTEXT (LAZY, NODE-ONLY)
65
+ // --------------------------------------------------
66
+ this.storage = null;
102
67
 
103
- // 4. Start Network Interceptor
104
- this.patchNetwork();
68
+ try {
69
+ const asyncHooks = require("async_hooks");
70
+ this.storage = new asyncHooks.AsyncLocalStorage();
71
+ } catch {
72
+ // Edge / Browser / Unsupported — safely disabled
73
+ this.storage = null;
74
+ }
105
75
 
106
- // Lock the instance
107
- Dropout.instance = this;
76
+ // --------------------------------------------------
77
+ // NETWORK PATCHING
78
+ // --------------------------------------------------
79
+ this.patchNetwork();
108
80
 
109
- } catch (err) {
110
- // THE ULTIMATE CATCH-ALL: Ensure app NEVER crashes
111
- if (config.debug) console.error("[Dropout] ❌ Initialization Error:", err);
81
+ if (this.debug) {
82
+ console.log(`[Dropout] đŸŸĸ Online | Project ${this.projectId}`);
112
83
  }
113
- }
114
84
 
115
- // --- UTILS ---
116
- log(msg, ...args) {
117
- if (this.debug) console.log(`[Dropout] ${msg}`, ...args);
85
+ Dropout.instance = this;
118
86
  }
119
87
 
88
+ // --------------------------------------------------
89
+ // UTILITIES
90
+ // --------------------------------------------------
120
91
  generateSessionId() {
121
92
  try {
122
93
  return crypto.randomUUID();
123
- } catch (e) {
124
- return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
125
- }
126
- }
127
-
128
- /**
129
- * 🔑 START NEW SESSION
130
- * Call this when user clicks "New Chat" or starts a new conversation thread.
131
- * Resets turn counter and generates a fresh session_id.
132
- * @param {string} [customSessionId] - Optional custom session ID, otherwise auto-generates
133
- * @returns {string} The new session ID
134
- */
135
- startNewSession(customSessionId) {
136
- this.currentSessionId = customSessionId || this.generateSessionId();
137
- this.turnIndex = 0; // Reset turn counter for new session
138
- this.lastTurnConfirmed = true;
139
- this.lastPromptHash = null;
140
- this.lastResponseHash = null;
141
- if (this.debug) {
142
- console.log(`[Dropout] 🔄 New Session Started: ${this.currentSessionId}`);
94
+ } catch {
95
+ return "sess_" + Date.now().toString(36);
143
96
  }
144
- return this.currentSessionId;
145
97
  }
146
98
 
147
99
  hash(text) {
148
100
  if (!text) return null;
149
101
  try {
150
- return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
151
- } catch (e) { return 'hash_err'; }
102
+ return crypto
103
+ .createHash("sha256")
104
+ .update(text.toLowerCase().trim())
105
+ .digest("hex");
106
+ } catch {
107
+ return "hash_err";
108
+ }
109
+ }
110
+
111
+ // --------------------------------------------------
112
+ // CONTEXT API (OPTIONAL BUT SAFE)
113
+ // --------------------------------------------------
114
+ run(sessionId, callback) {
115
+ if (!this.storage) return callback();
116
+
117
+ const context = {
118
+ sessionId: sessionId || this.generateSessionId(),
119
+ turnIndex: 0,
120
+ };
121
+
122
+ return this.storage.run(context, callback);
123
+ }
124
+
125
+ getContext() {
126
+ if (!this.storage) return null;
127
+ return this.storage.getStore() || null;
152
128
  }
153
129
 
130
+ getSessionId() {
131
+ const ctx = this.getContext();
132
+ return ctx?.sessionId || "global_session";
133
+ }
134
+
135
+ // --------------------------------------------------
136
+ // NORMALIZATION
137
+ // --------------------------------------------------
154
138
  normalize(url, body) {
155
- let provider = 'custom';
156
- let model = 'unknown';
139
+ let provider = "custom";
140
+ let model = "unknown";
141
+
157
142
  try {
158
- if (url) {
159
- const u = url.toLowerCase();
160
- if (u.includes('openai')) provider = 'openai';
161
- else if (u.includes('anthropic')) provider = 'anthropic';
162
- else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
163
- else if (u.includes('groq')) provider = 'groq';
164
- else if (u.includes('mistral')) provider = 'mistral';
165
- else if (u.includes('cohere')) provider = 'cohere';
166
- else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
167
- }
143
+ const u = url?.toLowerCase() || "";
144
+ if (u.includes("openai")) provider = "openai";
145
+ else if (u.includes("anthropic")) provider = "anthropic";
146
+ else if (u.includes("google")) provider = "google";
147
+ else if (u.includes("groq")) provider = "groq";
148
+ else if (u.includes("mistral")) provider = "mistral";
149
+ else if (u.includes("cohere")) provider = "cohere";
150
+
168
151
  if (body) {
169
- const parsed = typeof body === 'string' ? JSON.parse(body) : body;
170
- if (parsed.model) model = parsed.model;
171
- if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
152
+ const parsed = typeof body === "string" ? JSON.parse(body) : body;
153
+ if (parsed?.model) model = parsed.model;
172
154
  }
173
- } catch (e) { }
155
+ } catch { }
156
+
174
157
  return { provider, model };
175
158
  }
176
159
 
177
- isAiRequest(url, bodyString) {
178
- if (!url) return false;
179
- if (url.includes(this.captureEndpoint)) return false;
180
- if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
181
- if (bodyString) {
182
- return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
183
- (bodyString.includes('"user"') || bodyString.includes('"prompt"'));
160
+ isAiRequest(url, body) {
161
+ if (!url || url.includes(this.captureEndpoint)) return false;
162
+ if (KNOWN_AI_DOMAINS.some((d) => url.includes(d))) return true;
163
+
164
+ if (body) {
165
+ return (
166
+ body.includes('"model"') &&
167
+ (body.includes('"messages"') || body.includes('"prompt"'))
168
+ );
184
169
  }
185
170
  return false;
186
171
  }
187
172
 
188
- // --- EMITTER ---
173
+ // --------------------------------------------------
174
+ // EMITTER
175
+ // --------------------------------------------------
189
176
  emit(payload) {
190
177
  try {
191
- if (!this.apiKey || !this.projectId) return;
192
-
193
178
  const finalPayload = {
194
179
  project_id: this.projectId,
195
180
  session_id: payload.session_id,
196
181
  turn_index: payload.turn_index,
197
182
  turn_role: payload.turn_role,
198
- // ✅ DIRECTION LOGIC APPLIED HERE
199
- direction: payload.turn_role === 'user' ? 'user_to_ai' : 'ai_to_user',
183
+ direction:
184
+ payload.turn_role === "user" ? "user_to_ai" : "ai_to_user",
200
185
  provider: payload.provider,
201
186
  model: payload.model,
202
187
  latency_ms: payload.latency_ms || null,
203
188
  content: payload.content,
204
189
  content_hash: payload.content_hash,
205
190
  metadata_flags: payload.metadata_flags,
206
- received_at: new Date().toISOString()
191
+ received_at: new Date().toISOString(),
207
192
  };
208
193
 
209
- this.log(`🚀 Sending Capture [${payload.turn_role}]`);
210
-
211
- const isHttp = this.captureEndpoint.startsWith('http:');
212
- const requestModule = isHttp ? http : https;
213
-
214
- const req = requestModule.request(this.captureEndpoint, {
215
- method: 'POST',
194
+ const req = (this.captureEndpoint.startsWith("http:")
195
+ ? http
196
+ : https
197
+ ).request(this.captureEndpoint, {
198
+ method: "POST",
216
199
  headers: {
217
- 'Content-Type': 'application/json',
218
- 'x-dropout-key': this.apiKey
219
- }
200
+ "Content-Type": "application/json",
201
+ "x-dropout-key": this.apiKey,
202
+ },
220
203
  });
221
204
 
222
- req.on('error', (e) => this.log("❌ Upload Failed (Silent)", e.message));
205
+ req.on("error", () => { });
223
206
  req.write(JSON.stringify(finalPayload));
224
207
  req.end();
225
- } catch (e) {
226
- this.log("âš ī¸ Emit Error (Silent)", e);
227
- }
208
+ } catch { }
228
209
  }
229
210
 
230
- // --- CORE PATCHING ---
211
+ // --------------------------------------------------
212
+ // NETWORK PATCHING
213
+ // --------------------------------------------------
231
214
  patchNetwork() {
232
215
  const _this = this;
233
216
 
234
- // --- A. PATCH FETCH ---
235
- if (global.fetch) {
236
- const originalFetch = global.fetch;
237
- global.fetch = async function (input, init) {
238
- let url;
239
- try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
240
-
241
- let bodyStr = "";
242
- if (init && init.body) {
243
- try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
244
- }
217
+ if (!global.fetch) return;
245
218
 
246
- if (!_this.isAiRequest(url, bodyStr)) {
247
- return originalFetch.apply(this, arguments);
248
- }
219
+ const originalFetch = global.fetch;
249
220
 
250
- _this.log(`⚡ [FETCH] Intercepting: ${url}`);
251
- const start = Date.now();
252
-
253
- let activeTurn;
254
- if (_this.lastTurnConfirmed) {
255
- activeTurn = _this.turnIndex++;
256
- _this.lastTurnConfirmed = false;
257
- } else {
258
- activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
259
- }
260
-
261
- const { provider, model } = _this.normalize(url, bodyStr);
262
- const pHash = _this.hash(bodyStr);
263
-
264
- _this.emit({
265
- session_id: _this.currentSessionId,
266
- turn_index: activeTurn,
267
- turn_role: 'user',
268
- provider,
269
- model,
270
- content: _this.privacy === 'full' ? bodyStr : null,
271
- content_hash: pHash,
272
- metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: 'FETCH', url: url }
273
- });
274
- _this.lastPromptHash = pHash;
221
+ global.fetch = async function (input, init) {
222
+ let url;
223
+ try {
224
+ url = typeof input === "string" ? input : input?.url;
225
+ } catch {
226
+ return originalFetch.apply(this, arguments);
227
+ }
275
228
 
276
- let response;
229
+ let bodyStr = "";
230
+ if (init?.body) {
277
231
  try {
278
- response = await originalFetch.apply(this, arguments);
279
- } catch (err) { throw err; }
232
+ bodyStr =
233
+ typeof init.body === "string"
234
+ ? init.body
235
+ : JSON.stringify(init.body);
236
+ } catch { }
237
+ }
280
238
 
281
- const latency = Date.now() - start;
239
+ if (!_this.isAiRequest(url, bodyStr)) {
240
+ return originalFetch.apply(this, arguments);
241
+ }
282
242
 
283
- try {
284
- const cloned = response.clone();
285
- let oText = await cloned.text();
286
- if (oText && oText.length > _this.maxOutputBytes) oText = oText.slice(0, _this.maxOutputBytes);
287
- const oHash = _this.hash(oText);
288
-
289
- _this.emit({
290
- session_id: _this.currentSessionId,
291
- turn_index: activeTurn,
292
- turn_role: 'assistant',
293
- latency_ms: latency,
294
- provider,
295
- model,
296
- content: _this.privacy === 'full' ? oText : null,
297
- content_hash: oHash,
298
- metadata_flags: {
299
- non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
300
- turn_boundary_confirmed: 1,
301
- status: response.status
302
- }
303
- });
304
- _this.lastResponseHash = oHash;
305
- _this.lastTurnConfirmed = true;
306
- } catch (e) { _this.log("âš ī¸ Failed to read response body"); }
307
-
308
- return response;
309
- };
310
- this.log("✅ Patch Applied: global.fetch");
311
- }
243
+ const sessionId = _this.getSessionId();
244
+ const { provider, model } = _this.normalize(url, bodyStr);
245
+ const pHash = _this.hash(bodyStr);
246
+ const start = Date.now();
247
+
248
+ _this.emit({
249
+ session_id: sessionId,
250
+ turn_index: 0,
251
+ turn_role: "user",
252
+ provider,
253
+ model,
254
+ content: _this.privacy === "full" ? bodyStr : null,
255
+ content_hash: pHash,
256
+ metadata_flags: { method: "FETCH", url },
257
+ });
312
258
 
313
- // --- B. PATCH NODE HTTP/HTTPS ---
314
- const patchNodeRequest = (module, moduleName) => {
315
- const originalRequest = module.request;
316
- module.request = function (...args) {
317
- let url;
318
- try {
319
- if (typeof args[0] === 'string') {
320
- url = args[0];
321
- } else {
322
- const opts = args[0] || {};
323
- const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
324
- const host = opts.hostname || opts.host || 'localhost';
325
- const path = opts.path || '/';
326
- url = `${protocol}//${host}${path}`;
327
- }
328
- } catch (e) { return originalRequest.apply(this, args); }
329
-
330
- if (!_this.isAiRequest(url, null)) {
331
- return originalRequest.apply(this, args);
259
+ const response = await originalFetch.apply(this, arguments);
260
+
261
+ try {
262
+ const cloned = response.clone();
263
+ let oText = await cloned.text();
264
+ if (oText.length > _this.maxOutputBytes) {
265
+ oText = oText.slice(0, _this.maxOutputBytes);
332
266
  }
333
267
 
334
- _this.log(`⚡ [${moduleName.toUpperCase()}] Intercepting: ${url}`);
335
- const start = Date.now();
336
-
337
- const clientRequest = originalRequest.apply(this, args);
338
- const reqChunks = [];
339
- const originalWrite = clientRequest.write;
340
- const originalEnd = clientRequest.end;
341
-
342
- clientRequest.write = function (...writeArgs) {
343
- try {
344
- const chunk = writeArgs[0];
345
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
346
- reqChunks.push(Buffer.from(chunk));
347
- }
348
- } catch (e) { }
349
- return originalWrite.apply(this, writeArgs);
350
- };
351
-
352
- clientRequest.end = function (...endArgs) {
353
- try {
354
- const chunk = endArgs[0];
355
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
356
- reqChunks.push(Buffer.from(chunk));
357
- }
358
-
359
- const reqBody = Buffer.concat(reqChunks).toString('utf8');
360
- const { provider, model } = _this.normalize(url, reqBody);
361
- const pHash = _this.hash(reqBody);
362
-
363
- let activeTurn;
364
- if (_this.lastTurnConfirmed) {
365
- activeTurn = _this.turnIndex++;
366
- _this.lastTurnConfirmed = false;
367
- } else {
368
- activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
369
- }
370
-
371
- clientRequest._dropout_turn = activeTurn;
372
- clientRequest._dropout_provider = provider;
373
- clientRequest._dropout_model = model;
374
-
375
- _this.emit({
376
- session_id: _this.currentSessionId,
377
- turn_index: activeTurn,
378
- turn_role: 'user',
379
- provider,
380
- model,
381
- content: _this.privacy === 'full' ? reqBody : null,
382
- content_hash: pHash,
383
- metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: moduleName.toUpperCase(), url: url }
384
- });
385
- _this.lastPromptHash = pHash;
386
- } catch (e) { _this.log("âš ī¸ Request Capture Failed"); }
387
-
388
- return originalEnd.apply(this, endArgs);
389
- };
390
-
391
- clientRequest.on('response', (res) => {
392
- const resChunks = [];
393
- res.on('data', (chunk) => resChunks.push(chunk));
394
- res.on('end', () => {
395
- try {
396
- let resBody = Buffer.concat(resChunks).toString('utf8');
397
- if (resBody && resBody.length > _this.maxOutputBytes) resBody = resBody.slice(0, _this.maxOutputBytes);
398
-
399
- const latency = Date.now() - start;
400
- const oHash = _this.hash(resBody);
401
-
402
- _this.emit({
403
- session_id: _this.currentSessionId,
404
- turn_index: clientRequest._dropout_turn || 0,
405
- turn_role: 'assistant',
406
- latency_ms: latency,
407
- provider: clientRequest._dropout_provider,
408
- model: clientRequest._dropout_model,
409
- content: _this.privacy === 'full' ? resBody : null,
410
- content_hash: oHash,
411
- metadata_flags: { status: res.statusCode, non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0 }
412
- });
413
-
414
- _this.lastResponseHash = oHash;
415
- _this.lastTurnConfirmed = true;
416
- } catch (e) { _this.log("âš ī¸ Response Capture Failed"); }
417
- });
268
+ _this.emit({
269
+ session_id: sessionId,
270
+ turn_index: 1,
271
+ turn_role: "assistant",
272
+ latency_ms: Date.now() - start,
273
+ provider,
274
+ model,
275
+ content: _this.privacy === "full" ? oText : null,
276
+ content_hash: _this.hash(oText),
277
+ metadata_flags: { status: response.status },
418
278
  });
279
+ } catch { }
419
280
 
420
- return clientRequest;
421
- };
281
+ return response;
422
282
  };
423
-
424
- patchNodeRequest(https, 'https');
425
- patchNodeRequest(http, 'http');
426
- this.log("✅ Patch Applied: http/https");
427
283
  }
428
284
  }
429
285
 
430
- // Auto-Start (Backwards Compatibility & Environment Variable Support)
431
- if (typeof process !== 'undefined' && process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
286
+ // Auto-init via env (safe)
287
+ if (
288
+ typeof process !== "undefined" &&
289
+ process.env.DROPOUT_PROJECT_ID &&
290
+ process.env.DROPOUT_API_KEY
291
+ ) {
432
292
  Dropout.init({
433
293
  projectId: process.env.DROPOUT_PROJECT_ID,
434
294
  apiKey: process.env.DROPOUT_API_KEY,
435
- debug: process.env.DROPOUT_DEBUG === 'true'
436
295
  });
437
296
  }
438
297
 
439
- // đŸ›Ąī¸ DUAL EXPORT FIX (Crucial for TypeScript + Require compatibility)
440
298
  Dropout.default = Dropout;
441
- module.exports = Dropout;
299
+ module.exports = Dropout;