@dropout-ai/runtime 0.5.1 → 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.
- package/package.json +1 -1
- package/src/index.js +171 -158
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
7
|
let https, http, crypto;
|
|
8
|
-
const { AsyncLocalStorage } = require('async_hooks');
|
|
9
8
|
|
|
10
9
|
// --- DEFAULT CONFIGURATION ---
|
|
11
10
|
const SUPABASE_FUNCTION_URL =
|
|
@@ -13,43 +12,43 @@ 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
|
+
return;
|
|
38
|
+
}
|
|
40
39
|
|
|
41
|
-
// 🛡️
|
|
40
|
+
// 🛡️ Credential Guard
|
|
42
41
|
if (!config.apiKey || !config.projectId) {
|
|
43
42
|
if (config.debug) {
|
|
44
|
-
console.warn("[Dropout]
|
|
43
|
+
console.warn("[Dropout] Missing API Key or Project ID");
|
|
45
44
|
}
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
try {
|
|
50
|
-
https = require(
|
|
51
|
-
http = require(
|
|
52
|
-
crypto = require(
|
|
49
|
+
https = require("https");
|
|
50
|
+
http = require("http");
|
|
51
|
+
crypto = require("crypto");
|
|
53
52
|
} catch {
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
@@ -57,89 +56,100 @@ class Dropout {
|
|
|
57
56
|
this.projectId = config.projectId;
|
|
58
57
|
this.apiKey = config.apiKey;
|
|
59
58
|
this.debug = config.debug || false;
|
|
60
|
-
this.privacy = config.privacy ||
|
|
59
|
+
this.privacy = config.privacy || "full";
|
|
61
60
|
this.captureEndpoint = config.captureEndpoint || SUPABASE_FUNCTION_URL;
|
|
62
61
|
this.maxOutputBytes = 32768;
|
|
63
62
|
|
|
64
|
-
//
|
|
65
|
-
|
|
63
|
+
// --------------------------------------------------
|
|
64
|
+
// ✅ ASYNC CONTEXT (LAZY, NODE-ONLY)
|
|
65
|
+
// --------------------------------------------------
|
|
66
|
+
this.storage = null;
|
|
67
|
+
|
|
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
|
+
}
|
|
66
75
|
|
|
76
|
+
// --------------------------------------------------
|
|
77
|
+
// NETWORK PATCHING
|
|
78
|
+
// --------------------------------------------------
|
|
67
79
|
this.patchNetwork();
|
|
68
80
|
|
|
69
81
|
if (this.debug) {
|
|
70
|
-
console.log(`[Dropout] 🟢 Online | Project
|
|
82
|
+
console.log(`[Dropout] 🟢 Online | Project ${this.projectId}`);
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
Dropout.instance = this;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
log(msg, ...args) {
|
|
80
|
-
if (this.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
81
|
-
}
|
|
82
|
-
|
|
88
|
+
// --------------------------------------------------
|
|
89
|
+
// UTILITIES
|
|
90
|
+
// --------------------------------------------------
|
|
83
91
|
generateSessionId() {
|
|
84
92
|
try {
|
|
85
93
|
return crypto.randomUUID();
|
|
86
94
|
} catch {
|
|
87
|
-
return
|
|
95
|
+
return "sess_" + Date.now().toString(36);
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
hash(text) {
|
|
92
100
|
if (!text) return null;
|
|
93
101
|
try {
|
|
94
|
-
return crypto
|
|
102
|
+
return crypto
|
|
103
|
+
.createHash("sha256")
|
|
95
104
|
.update(text.toLowerCase().trim())
|
|
96
|
-
.digest(
|
|
105
|
+
.digest("hex");
|
|
97
106
|
} catch {
|
|
98
|
-
return
|
|
107
|
+
return "hash_err";
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
// --------------------------------------------------
|
|
112
|
+
// CONTEXT API (OPTIONAL BUT SAFE)
|
|
113
|
+
// --------------------------------------------------
|
|
114
|
+
run(sessionId, callback) {
|
|
115
|
+
if (!this.storage) return callback();
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
turn_index: 0,
|
|
113
|
-
last_prompt_hash: null,
|
|
114
|
-
last_response_hash: null,
|
|
115
|
-
last_turn_confirmed: true
|
|
116
|
-
};
|
|
117
|
-
this.ctx.enterWith(store);
|
|
118
|
-
}
|
|
117
|
+
const context = {
|
|
118
|
+
sessionId: sessionId || this.generateSessionId(),
|
|
119
|
+
turnIndex: 0,
|
|
120
|
+
};
|
|
119
121
|
|
|
120
|
-
return
|
|
122
|
+
return this.storage.run(context, callback);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
getContext() {
|
|
126
|
+
if (!this.storage) return null;
|
|
127
|
+
return this.storage.getStore() || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getSessionId() {
|
|
131
|
+
const ctx = this.getContext();
|
|
132
|
+
return ctx?.sessionId || "global_session";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --------------------------------------------------
|
|
136
|
+
// NORMALIZATION
|
|
137
|
+
// --------------------------------------------------
|
|
126
138
|
normalize(url, body) {
|
|
127
|
-
let provider =
|
|
128
|
-
let model =
|
|
139
|
+
let provider = "custom";
|
|
140
|
+
let model = "unknown";
|
|
129
141
|
|
|
130
142
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else if (u.includes('cohere')) provider = 'cohere';
|
|
139
|
-
}
|
|
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";
|
|
140
150
|
|
|
141
151
|
if (body) {
|
|
142
|
-
const parsed = typeof body ===
|
|
152
|
+
const parsed = typeof body === "string" ? JSON.parse(body) : body;
|
|
143
153
|
if (parsed?.model) model = parsed.model;
|
|
144
154
|
}
|
|
145
155
|
} catch { }
|
|
@@ -148,9 +158,8 @@ class Dropout {
|
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
isAiRequest(url, body) {
|
|
151
|
-
if (!url) return false;
|
|
152
|
-
if (url.includes(
|
|
153
|
-
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
161
|
+
if (!url || url.includes(this.captureEndpoint)) return false;
|
|
162
|
+
if (KNOWN_AI_DOMAINS.some((d) => url.includes(d))) return true;
|
|
154
163
|
|
|
155
164
|
if (body) {
|
|
156
165
|
return (
|
|
@@ -158,127 +167,131 @@ class Dropout {
|
|
|
158
167
|
(body.includes('"messages"') || body.includes('"prompt"'))
|
|
159
168
|
);
|
|
160
169
|
}
|
|
161
|
-
|
|
162
170
|
return false;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
173
|
+
// --------------------------------------------------
|
|
174
|
+
// EMITTER
|
|
175
|
+
// --------------------------------------------------
|
|
168
176
|
emit(payload) {
|
|
169
177
|
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({
|
|
178
|
+
const finalPayload = {
|
|
181
179
|
project_id: this.projectId,
|
|
180
|
+
session_id: payload.session_id,
|
|
181
|
+
turn_index: payload.turn_index,
|
|
182
|
+
turn_role: payload.turn_role,
|
|
183
|
+
direction:
|
|
184
|
+
payload.turn_role === "user" ? "user_to_ai" : "ai_to_user",
|
|
185
|
+
provider: payload.provider,
|
|
186
|
+
model: payload.model,
|
|
187
|
+
latency_ms: payload.latency_ms || null,
|
|
188
|
+
content: payload.content,
|
|
189
|
+
content_hash: payload.content_hash,
|
|
190
|
+
metadata_flags: payload.metadata_flags,
|
|
182
191
|
received_at: new Date().toISOString(),
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const req = (this.captureEndpoint.startsWith("http:")
|
|
195
|
+
? http
|
|
196
|
+
: https
|
|
197
|
+
).request(this.captureEndpoint, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"x-dropout-key": this.apiKey,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
req.on("error", () => { });
|
|
206
|
+
req.write(JSON.stringify(finalPayload));
|
|
185
207
|
req.end();
|
|
186
208
|
} catch { }
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
211
|
+
// --------------------------------------------------
|
|
212
|
+
// NETWORK PATCHING
|
|
213
|
+
// --------------------------------------------------
|
|
192
214
|
patchNetwork() {
|
|
193
215
|
const _this = this;
|
|
194
216
|
|
|
195
|
-
|
|
196
|
-
if (global.fetch) {
|
|
197
|
-
const originalFetch = global.fetch;
|
|
217
|
+
if (!global.fetch) return;
|
|
198
218
|
|
|
199
|
-
|
|
200
|
-
let url;
|
|
201
|
-
try { url = typeof input === 'string' ? input : input?.url; }
|
|
202
|
-
catch { return originalFetch.apply(this, arguments); }
|
|
219
|
+
const originalFetch = global.fetch;
|
|
203
220
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
let bodyStr = "";
|
|
230
|
+
if (init?.body) {
|
|
231
|
+
try {
|
|
232
|
+
bodyStr =
|
|
233
|
+
typeof init.body === "string"
|
|
234
|
+
? init.body
|
|
235
|
+
: JSON.stringify(init.body);
|
|
236
|
+
} catch { }
|
|
237
|
+
}
|
|
213
238
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const pHash = _this.hash(bodyStr);
|
|
239
|
+
if (!_this.isAiRequest(url, bodyStr)) {
|
|
240
|
+
return originalFetch.apply(this, arguments);
|
|
241
|
+
}
|
|
218
242
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
243
|
+
const sessionId = _this.getSessionId();
|
|
244
|
+
const { provider, model } = _this.normalize(url, bodyStr);
|
|
245
|
+
const pHash = _this.hash(bodyStr);
|
|
246
|
+
const start = Date.now();
|
|
223
247
|
|
|
224
|
-
|
|
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
|
+
});
|
|
258
|
+
|
|
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);
|
|
266
|
+
}
|
|
225
267
|
|
|
226
268
|
_this.emit({
|
|
227
|
-
session_id:
|
|
228
|
-
turn_index:
|
|
229
|
-
turn_role:
|
|
230
|
-
|
|
269
|
+
session_id: sessionId,
|
|
270
|
+
turn_index: 1,
|
|
271
|
+
turn_role: "assistant",
|
|
272
|
+
latency_ms: Date.now() - start,
|
|
231
273
|
provider,
|
|
232
274
|
model,
|
|
233
|
-
content: _this.privacy ===
|
|
234
|
-
content_hash:
|
|
235
|
-
metadata_flags: {
|
|
236
|
-
retry_like: pHash === session.last_prompt_hash ? 1 : 0
|
|
237
|
-
}
|
|
275
|
+
content: _this.privacy === "full" ? oText : null,
|
|
276
|
+
content_hash: _this.hash(oText),
|
|
277
|
+
metadata_flags: { status: response.status },
|
|
238
278
|
});
|
|
279
|
+
} catch { }
|
|
239
280
|
|
|
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
|
-
}
|
|
281
|
+
return response;
|
|
282
|
+
};
|
|
273
283
|
}
|
|
274
284
|
}
|
|
275
285
|
|
|
276
|
-
// Auto-init
|
|
277
|
-
if (
|
|
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
|
+
) {
|
|
278
292
|
Dropout.init({
|
|
279
293
|
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
280
294
|
apiKey: process.env.DROPOUT_API_KEY,
|
|
281
|
-
debug: process.env.DROPOUT_DEBUG === 'true'
|
|
282
295
|
});
|
|
283
296
|
}
|
|
284
297
|
|