@dropout-ai/runtime 0.5.1 → 0.5.3
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 +217 -159
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Universal AI Interaction Capture for Node.js
|
|
4
|
-
* Stability:
|
|
4
|
+
* Stability: High (Browser-Safe, Next.js Safe)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
let https, http, crypto;
|
|
8
|
-
const { AsyncLocalStorage } = require('async_hooks');
|
|
7
|
+
let https, http, crypto, AsyncLocalStorage;
|
|
9
8
|
|
|
10
9
|
// --- DEFAULT CONFIGURATION ---
|
|
11
10
|
const SUPABASE_FUNCTION_URL =
|
|
@@ -13,133 +12,141 @@ const SUPABASE_FUNCTION_URL =
|
|
|
13
12
|
|
|
14
13
|
// Known AI Domains
|
|
15
14
|
const KNOWN_AI_DOMAINS = [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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",
|
|
23
22
|
];
|
|
24
23
|
|
|
25
24
|
class Dropout {
|
|
26
25
|
static instance = null;
|
|
27
26
|
|
|
28
27
|
static init(config) {
|
|
29
|
-
if (!Dropout.instance)
|
|
30
|
-
new Dropout(config);
|
|
31
|
-
}
|
|
28
|
+
if (!Dropout.instance) new Dropout(config);
|
|
32
29
|
return Dropout.instance;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
constructor(config = {}) {
|
|
36
33
|
if (Dropout.instance) return Dropout.instance;
|
|
37
34
|
|
|
38
|
-
// 🛡️ Browser Guard
|
|
39
|
-
if (typeof window !==
|
|
35
|
+
// 🛡️ Browser Guard (prevents client execution)
|
|
36
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
37
|
+
if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Browser detected. SDK is Server-Only.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
40
|
|
|
41
|
-
// 🛡️
|
|
41
|
+
// 🛡️ Credential Guard
|
|
42
42
|
if (!config.apiKey || !config.projectId) {
|
|
43
43
|
if (config.debug) {
|
|
44
|
-
console.warn("[Dropout]
|
|
44
|
+
console.warn("[Dropout] Missing API Key or Project ID");
|
|
45
45
|
}
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// 🛡️ Safe Dependency Import
|
|
49
50
|
try {
|
|
50
|
-
https = require(
|
|
51
|
-
http = require(
|
|
52
|
-
crypto = require(
|
|
51
|
+
https = require("https");
|
|
52
|
+
http = require("http");
|
|
53
|
+
crypto = require("crypto");
|
|
54
|
+
// Import async_hooks dynamically to avoid browser build crashes
|
|
55
|
+
const asyncHooks = require("async_hooks");
|
|
56
|
+
AsyncLocalStorage = asyncHooks.AsyncLocalStorage;
|
|
53
57
|
} catch {
|
|
58
|
+
if (config.debug) console.warn("[Dropout] ⚠️ Node core modules missing.");
|
|
54
59
|
return;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
this.projectId = config.projectId;
|
|
58
63
|
this.apiKey = config.apiKey;
|
|
59
64
|
this.debug = config.debug || false;
|
|
60
|
-
this.privacy = config.privacy ||
|
|
65
|
+
this.privacy = config.privacy || "full";
|
|
61
66
|
this.captureEndpoint = config.captureEndpoint || SUPABASE_FUNCTION_URL;
|
|
62
67
|
this.maxOutputBytes = 32768;
|
|
63
68
|
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
// --------------------------------------------------
|
|
70
|
+
// ✅ ASYNC CONTEXT (LAZY, NODE-ONLY)
|
|
71
|
+
// --------------------------------------------------
|
|
72
|
+
this.storage = null;
|
|
73
|
+
if (AsyncLocalStorage) {
|
|
74
|
+
this.storage = new AsyncLocalStorage();
|
|
75
|
+
}
|
|
66
76
|
|
|
77
|
+
// --------------------------------------------------
|
|
78
|
+
// NETWORK PATCHING
|
|
79
|
+
// --------------------------------------------------
|
|
67
80
|
this.patchNetwork();
|
|
68
81
|
|
|
69
82
|
if (this.debug) {
|
|
70
|
-
console.log(`[Dropout] 🟢 Online | Project
|
|
83
|
+
console.log(`[Dropout] 🟢 Online | Project ${this.projectId}`);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
Dropout.instance = this;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
log(msg, ...args) {
|
|
80
|
-
if (this.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
81
|
-
}
|
|
82
|
-
|
|
89
|
+
// --------------------------------------------------
|
|
90
|
+
// UTILITIES
|
|
91
|
+
// --------------------------------------------------
|
|
83
92
|
generateSessionId() {
|
|
84
93
|
try {
|
|
85
94
|
return crypto.randomUUID();
|
|
86
95
|
} catch {
|
|
87
|
-
return
|
|
96
|
+
return "sess_" + Date.now().toString(36);
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
hash(text) {
|
|
92
101
|
if (!text) return null;
|
|
93
102
|
try {
|
|
94
|
-
return crypto
|
|
103
|
+
return crypto
|
|
104
|
+
.createHash("sha256")
|
|
95
105
|
.update(text.toLowerCase().trim())
|
|
96
|
-
.digest(
|
|
106
|
+
.digest("hex");
|
|
97
107
|
} catch {
|
|
98
|
-
return
|
|
108
|
+
return "hash_err";
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
// --------------------------------------------------
|
|
113
|
+
// CONTEXT API (OPTIONAL BUT SAFE)
|
|
114
|
+
// --------------------------------------------------
|
|
115
|
+
run(sessionId, callback) {
|
|
116
|
+
// If storage isn't available, just run the callback
|
|
117
|
+
if (!this.storage) return callback();
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
last_turn_confirmed: true
|
|
116
|
-
};
|
|
117
|
-
this.ctx.enterWith(store);
|
|
118
|
-
}
|
|
119
|
+
const context = {
|
|
120
|
+
sessionId: sessionId || this.generateSessionId(),
|
|
121
|
+
turnIndex: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return this.storage.run(context, callback);
|
|
125
|
+
}
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
getContext() {
|
|
128
|
+
if (!this.storage) return null;
|
|
129
|
+
return this.storage.getStore() || null;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
132
|
+
// --------------------------------------------------
|
|
133
|
+
// NORMALIZATION
|
|
134
|
+
// --------------------------------------------------
|
|
126
135
|
normalize(url, body) {
|
|
127
|
-
let provider =
|
|
128
|
-
let model =
|
|
136
|
+
let provider = "custom";
|
|
137
|
+
let model = "unknown";
|
|
129
138
|
|
|
130
139
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else if (u.includes('cohere')) provider = 'cohere';
|
|
139
|
-
}
|
|
140
|
+
const u = url?.toLowerCase() || "";
|
|
141
|
+
if (u.includes("openai")) provider = "openai";
|
|
142
|
+
else if (u.includes("anthropic")) provider = "anthropic";
|
|
143
|
+
else if (u.includes("google")) provider = "google";
|
|
144
|
+
else if (u.includes("groq")) provider = "groq";
|
|
145
|
+
else if (u.includes("mistral")) provider = "mistral";
|
|
146
|
+
else if (u.includes("cohere")) provider = "cohere";
|
|
140
147
|
|
|
141
148
|
if (body) {
|
|
142
|
-
const parsed = typeof body ===
|
|
149
|
+
const parsed = typeof body === "string" ? JSON.parse(body) : body;
|
|
143
150
|
if (parsed?.model) model = parsed.model;
|
|
144
151
|
}
|
|
145
152
|
} catch { }
|
|
@@ -148,9 +155,8 @@ class Dropout {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
isAiRequest(url, body) {
|
|
151
|
-
if (!url) return false;
|
|
152
|
-
if (url.includes(
|
|
153
|
-
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
158
|
+
if (!url || url.includes(this.captureEndpoint)) return false;
|
|
159
|
+
if (KNOWN_AI_DOMAINS.some((d) => url.includes(d))) return true;
|
|
154
160
|
|
|
155
161
|
if (body) {
|
|
156
162
|
return (
|
|
@@ -158,129 +164,181 @@ class Dropout {
|
|
|
158
164
|
(body.includes('"messages"') || body.includes('"prompt"'))
|
|
159
165
|
);
|
|
160
166
|
}
|
|
161
|
-
|
|
162
167
|
return false;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
170
|
+
// --------------------------------------------------
|
|
171
|
+
// EMITTER
|
|
172
|
+
// --------------------------------------------------
|
|
168
173
|
emit(payload) {
|
|
169
174
|
try {
|
|
170
|
-
const
|
|
171
|
-
.request(this.captureEndpoint, {
|
|
172
|
-
method: 'POST',
|
|
173
|
-
headers: {
|
|
174
|
-
'Content-Type': 'application/json',
|
|
175
|
-
'x-dropout-key': this.apiKey
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
req.on('error', () => { });
|
|
180
|
-
req.write(JSON.stringify({
|
|
175
|
+
const finalPayload = {
|
|
181
176
|
project_id: this.projectId,
|
|
177
|
+
session_id: payload.session_id,
|
|
178
|
+
turn_index: payload.turn_index,
|
|
179
|
+
turn_role: payload.turn_role,
|
|
180
|
+
direction:
|
|
181
|
+
payload.turn_role === "user" ? "user_to_ai" : "ai_to_user",
|
|
182
|
+
provider: payload.provider,
|
|
183
|
+
model: payload.model,
|
|
184
|
+
latency_ms: payload.latency_ms || null,
|
|
185
|
+
content: payload.content,
|
|
186
|
+
content_hash: payload.content_hash,
|
|
187
|
+
metadata_flags: payload.metadata_flags,
|
|
182
188
|
received_at: new Date().toISOString(),
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const requestModule = this.captureEndpoint.startsWith("http:")
|
|
192
|
+
? http
|
|
193
|
+
: https;
|
|
194
|
+
|
|
195
|
+
const req = requestModule.request(this.captureEndpoint, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"x-dropout-key": this.apiKey,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
req.on("error", () => { });
|
|
204
|
+
req.write(JSON.stringify(finalPayload));
|
|
185
205
|
req.end();
|
|
186
206
|
} catch { }
|
|
187
207
|
}
|
|
188
208
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
209
|
+
// --------------------------------------------------
|
|
210
|
+
// NETWORK PATCHING (INTELLIGENT)
|
|
211
|
+
// --------------------------------------------------
|
|
192
212
|
patchNetwork() {
|
|
193
213
|
const _this = this;
|
|
194
214
|
|
|
195
|
-
|
|
196
|
-
if (global.fetch) {
|
|
197
|
-
const originalFetch = global.fetch;
|
|
215
|
+
if (!global.fetch) return;
|
|
198
216
|
|
|
199
|
-
|
|
200
|
-
let url;
|
|
201
|
-
try { url = typeof input === 'string' ? input : input?.url; }
|
|
202
|
-
catch { return originalFetch.apply(this, arguments); }
|
|
217
|
+
const originalFetch = global.fetch;
|
|
203
218
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
global.fetch = async function (input, init) {
|
|
220
|
+
let url;
|
|
221
|
+
try {
|
|
222
|
+
url = typeof input === "string" ? input : input?.url;
|
|
223
|
+
} catch {
|
|
224
|
+
return originalFetch.apply(this, arguments);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let bodyStr = "";
|
|
228
|
+
let parsedBody = null;
|
|
229
|
+
if (init?.body) {
|
|
230
|
+
try {
|
|
231
|
+
bodyStr =
|
|
232
|
+
typeof init.body === "string"
|
|
233
|
+
? init.body
|
|
234
|
+
: JSON.stringify(init.body);
|
|
235
|
+
parsedBody = JSON.parse(bodyStr);
|
|
236
|
+
} catch { }
|
|
237
|
+
}
|
|
209
238
|
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
if (!_this.isAiRequest(url, bodyStr)) {
|
|
240
|
+
return originalFetch.apply(this, arguments);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 🕵️ 1. SESSION LOGIC (PRIORITY ORDER)
|
|
244
|
+
// A. Explicit Context (if wrapped in .run())
|
|
245
|
+
const ctx = _this.getContext();
|
|
246
|
+
let activeSessionId = ctx?.sessionId;
|
|
247
|
+
|
|
248
|
+
if (!activeSessionId && parsedBody) {
|
|
249
|
+
// B. Explicit Override in Payload
|
|
250
|
+
const explicitId =
|
|
251
|
+
parsedBody.session_id ||
|
|
252
|
+
parsedBody.metadata?.session_id ||
|
|
253
|
+
parsedBody.metadata?.sessionId;
|
|
254
|
+
|
|
255
|
+
if (explicitId) {
|
|
256
|
+
activeSessionId = explicitId;
|
|
257
|
+
} else {
|
|
258
|
+
// C. SMART FINGERPRINTING (The Fix)
|
|
259
|
+
const userId =
|
|
260
|
+
parsedBody.user ||
|
|
261
|
+
parsedBody.userId ||
|
|
262
|
+
parsedBody.metadata?.user_id ||
|
|
263
|
+
"anon_user";
|
|
264
|
+
|
|
265
|
+
let rootContext = "";
|
|
266
|
+
// Grab first 64 chars of the FIRST message as "Conversation Anchor"
|
|
267
|
+
if (parsedBody.messages?.length > 0) {
|
|
268
|
+
rootContext = parsedBody.messages[0].content?.slice(0, 64) || "";
|
|
269
|
+
} else if (parsedBody.prompt) {
|
|
270
|
+
rootContext =
|
|
271
|
+
typeof parsedBody.prompt === "string"
|
|
272
|
+
? parsedBody.prompt.slice(0, 64)
|
|
273
|
+
: JSON.stringify(parsedBody.prompt).slice(0, 64);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (rootContext) {
|
|
277
|
+
// Salt with Date (YYYY-MM-DD) to differentiate sessions across days
|
|
278
|
+
const dateSalt = new Date().toISOString().slice(0, 10);
|
|
279
|
+
activeSessionId =
|
|
280
|
+
"sess_" +
|
|
281
|
+
_this.hash(`${userId}::${rootContext}::${dateSalt}`).substring(0, 12);
|
|
282
|
+
}
|
|
212
283
|
}
|
|
284
|
+
}
|
|
213
285
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const { provider, model } = _this.normalize(url, bodyStr);
|
|
217
|
-
const pHash = _this.hash(bodyStr);
|
|
286
|
+
// D. Fallback
|
|
287
|
+
activeSessionId = activeSessionId || _this.generateSessionId();
|
|
218
288
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: Math.max(session.turn_index - 1, 0);
|
|
289
|
+
const { provider, model } = _this.normalize(url, bodyStr);
|
|
290
|
+
const pHash = _this.hash(bodyStr);
|
|
291
|
+
const start = Date.now();
|
|
223
292
|
|
|
224
|
-
|
|
293
|
+
_this.emit({
|
|
294
|
+
session_id: activeSessionId,
|
|
295
|
+
turn_index: 0,
|
|
296
|
+
turn_role: "user",
|
|
297
|
+
provider,
|
|
298
|
+
model,
|
|
299
|
+
content: _this.privacy === "full" ? bodyStr : null,
|
|
300
|
+
content_hash: pHash,
|
|
301
|
+
metadata_flags: { method: "FETCH", url },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const response = await originalFetch.apply(this, arguments);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const cloned = response.clone();
|
|
308
|
+
let oText = await cloned.text();
|
|
309
|
+
if (oText.length > _this.maxOutputBytes) {
|
|
310
|
+
oText = oText.slice(0, _this.maxOutputBytes);
|
|
311
|
+
}
|
|
225
312
|
|
|
226
313
|
_this.emit({
|
|
227
|
-
session_id:
|
|
228
|
-
turn_index:
|
|
229
|
-
turn_role:
|
|
230
|
-
|
|
314
|
+
session_id: activeSessionId,
|
|
315
|
+
turn_index: 1,
|
|
316
|
+
turn_role: "assistant",
|
|
317
|
+
latency_ms: Date.now() - start,
|
|
231
318
|
provider,
|
|
232
319
|
model,
|
|
233
|
-
content: _this.privacy ===
|
|
234
|
-
content_hash:
|
|
235
|
-
metadata_flags: {
|
|
236
|
-
retry_like: pHash === session.last_prompt_hash ? 1 : 0
|
|
237
|
-
}
|
|
320
|
+
content: _this.privacy === "full" ? oText : null,
|
|
321
|
+
content_hash: _this.hash(oText),
|
|
322
|
+
metadata_flags: { status: response.status },
|
|
238
323
|
});
|
|
324
|
+
} catch { }
|
|
239
325
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const response = await originalFetch.apply(this, arguments);
|
|
243
|
-
const latency = Date.now() - start;
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const text = await response.clone().text();
|
|
247
|
-
const oHash = _this.hash(text);
|
|
248
|
-
|
|
249
|
-
_this.emit({
|
|
250
|
-
session_id: session.session_id,
|
|
251
|
-
turn_index: turn,
|
|
252
|
-
turn_role: 'assistant',
|
|
253
|
-
direction: 'ai_to_user',
|
|
254
|
-
provider,
|
|
255
|
-
model,
|
|
256
|
-
latency_ms: latency,
|
|
257
|
-
content: _this.privacy === 'full' ? text.slice(0, _this.maxOutputBytes) : null,
|
|
258
|
-
content_hash: oHash,
|
|
259
|
-
metadata_flags: {
|
|
260
|
-
non_adaptive_response: oHash === session.last_response_hash ? 1 : 0,
|
|
261
|
-
turn_boundary_confirmed: 1,
|
|
262
|
-
status: response.status
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
session.last_response_hash = oHash;
|
|
267
|
-
session.last_turn_confirmed = true;
|
|
268
|
-
} catch { }
|
|
269
|
-
|
|
270
|
-
return response;
|
|
271
|
-
};
|
|
272
|
-
}
|
|
326
|
+
return response;
|
|
327
|
+
};
|
|
273
328
|
}
|
|
274
329
|
}
|
|
275
330
|
|
|
276
|
-
// Auto-init
|
|
277
|
-
if (
|
|
331
|
+
// Auto-init via env (safe)
|
|
332
|
+
if (
|
|
333
|
+
typeof process !== "undefined" &&
|
|
334
|
+
process.env.DROPOUT_PROJECT_ID &&
|
|
335
|
+
process.env.DROPOUT_API_KEY
|
|
336
|
+
) {
|
|
278
337
|
Dropout.init({
|
|
279
338
|
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
280
339
|
apiKey: process.env.DROPOUT_API_KEY,
|
|
281
|
-
debug: process.env.DROPOUT_DEBUG === 'true'
|
|
282
340
|
});
|
|
283
341
|
}
|
|
284
342
|
|
|
285
343
|
Dropout.default = Dropout;
|
|
286
|
-
module.exports = Dropout;
|
|
344
|
+
module.exports = Dropout;
|