@dropout-ai/runtime 0.2.13 → 0.3.0
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 +95 -189
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,115 +2,48 @@
|
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Role: Passive observer.
|
|
4
4
|
* Behavior: Fire-and-forget raw JSON to Supabase.
|
|
5
|
-
* Authentication:
|
|
5
|
+
* Authentication: API Key + Project ID.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
|
|
10
|
-
// --- CONFIGURATION ---
|
|
11
|
-
// Public ingestion endpoint (No keys required)
|
|
10
|
+
// --- DEFAULT CONFIGURATION ---
|
|
12
11
|
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
13
12
|
|
|
14
|
-
//
|
|
13
|
+
// Global State
|
|
15
14
|
const GLOBAL_OBJ = typeof window !== 'undefined' ? window : global;
|
|
15
|
+
let activeConfig = {
|
|
16
|
+
projectId: null,
|
|
17
|
+
apiKey: null,
|
|
18
|
+
captureEndpoint: SUPABASE_FUNCTION_URL,
|
|
19
|
+
maxOutputBytes: 32768,
|
|
20
|
+
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full'
|
|
21
|
+
};
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
return crypto.randomUUID();
|
|
20
|
-
} catch (e) {
|
|
21
|
-
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
26
|
-
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
27
|
-
}
|
|
28
|
-
|
|
23
|
+
let isPatched = false;
|
|
29
24
|
let turnIndex = 0;
|
|
30
25
|
let lastTurnConfirmed = true;
|
|
31
26
|
let lastPromptHash = null;
|
|
32
27
|
let lastResponseHash = null;
|
|
28
|
+
let instance = null;
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
maxOutputBytes: 32768,
|
|
36
|
-
captureEndpoint: SUPABASE_FUNCTION_URL,
|
|
37
|
-
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full'
|
|
38
|
-
};
|
|
30
|
+
// --- UTILS ---
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.log("[Dropout Debug] emit() called with payload:", JSON.stringify(payload).slice(0, 100) + "...");
|
|
47
|
-
const fetchFn = GLOBAL_OBJ.__dropout_original_fetch__ || GLOBAL_OBJ.fetch;
|
|
48
|
-
if (typeof fetchFn !== 'function') return;
|
|
49
|
-
|
|
50
|
-
setTimeout(() => {
|
|
51
|
-
fetchFn(config.captureEndpoint, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: {
|
|
54
|
-
'Content-Type': 'application/json'
|
|
55
|
-
// No Authorization header needed since Enforce JWT is OFF
|
|
56
|
-
},
|
|
57
|
-
body: JSON.stringify(payload),
|
|
58
|
-
keepalive: true
|
|
59
|
-
}).then(res => {
|
|
60
|
-
console.log(`[Dropout Debug] Supabase Response: ${res.status} ${res.statusText}`);
|
|
61
|
-
if (!res.ok) {
|
|
62
|
-
res.text().then(t => console.error("[Dropout Debug] Error Body:", t));
|
|
63
|
-
}
|
|
64
|
-
}).catch(() => {
|
|
65
|
-
// Silent fail (Fire & Forget) to ensure no impact on host app
|
|
66
|
-
console.error("[Dropout Debug] NETWORK ERROR:", err);
|
|
67
|
-
});
|
|
68
|
-
}, 0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// --- Lifecycle Signals ---
|
|
72
|
-
|
|
73
|
-
function emitSessionEnd(reason) {
|
|
74
|
-
if (GLOBAL_OBJ.__dropout_session_ended__) return;
|
|
75
|
-
GLOBAL_OBJ.__dropout_session_ended__ = true;
|
|
76
|
-
|
|
77
|
-
emit({
|
|
78
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
79
|
-
turn_index: turnIndex,
|
|
80
|
-
direction: 'meta',
|
|
81
|
-
turn_role: 'system',
|
|
82
|
-
metadata_flags: {
|
|
83
|
-
session_end: true,
|
|
84
|
-
end_reason: reason
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Browser: Navigation/Reload
|
|
90
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
91
|
-
window.addEventListener('beforeunload', () => emitSessionEnd('navigation'));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Node.js: Process Exit
|
|
95
|
-
if (typeof process !== 'undefined' && process.on) {
|
|
96
|
-
process.on('exit', () => emitSessionEnd('process_exit'));
|
|
97
|
-
process.on('SIGINT', () => { emitSessionEnd('sigint'); process.exit(); });
|
|
98
|
-
process.on('SIGTERM', () => { emitSessionEnd('sigterm'); process.exit(); });
|
|
32
|
+
function generateSessionId() {
|
|
33
|
+
try {
|
|
34
|
+
return crypto.randomUUID();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
37
|
+
}
|
|
99
38
|
}
|
|
100
39
|
|
|
101
|
-
// --- Content Utilities ---
|
|
102
|
-
|
|
103
40
|
function hash(text) {
|
|
104
41
|
if (!text) return null;
|
|
105
42
|
try {
|
|
106
43
|
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
107
|
-
} catch (e) {
|
|
108
|
-
return 'hash_err';
|
|
109
|
-
}
|
|
44
|
+
} catch (e) { return 'hash_err'; }
|
|
110
45
|
}
|
|
111
46
|
|
|
112
|
-
// --- Provider Normalization ---
|
|
113
|
-
|
|
114
47
|
function normalize(url, body) {
|
|
115
48
|
let provider = 'unknown';
|
|
116
49
|
let model = 'unknown';
|
|
@@ -133,20 +66,49 @@ function normalize(url, body) {
|
|
|
133
66
|
}
|
|
134
67
|
} catch (e) { }
|
|
135
68
|
}
|
|
136
|
-
|
|
137
69
|
return { provider, model };
|
|
138
70
|
}
|
|
139
71
|
|
|
140
|
-
// ---
|
|
72
|
+
// --- EMITTER (Authenticated) ---
|
|
73
|
+
|
|
74
|
+
function emit(payload) {
|
|
75
|
+
// 1. Guard: Do not emit if not initialized
|
|
76
|
+
if (!activeConfig.apiKey || !activeConfig.projectId) return;
|
|
77
|
+
|
|
78
|
+
const fetchFn = GLOBAL_OBJ.__dropout_original_fetch__ || GLOBAL_OBJ.fetch;
|
|
79
|
+
if (typeof fetchFn !== 'function') return;
|
|
80
|
+
|
|
81
|
+
// 2. Attach Project ID to payload
|
|
82
|
+
const finalPayload = {
|
|
83
|
+
...payload,
|
|
84
|
+
project_id: activeConfig.projectId
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
fetchFn(activeConfig.captureEndpoint, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'x-dropout-key': activeConfig.apiKey // <--- AUTH HEADER
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify(finalPayload),
|
|
95
|
+
keepalive: true
|
|
96
|
+
}).catch(() => { /* Silent Fail */ });
|
|
97
|
+
}, 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- MONKEY PATCH ---
|
|
101
|
+
|
|
102
|
+
function applyPatch() {
|
|
103
|
+
if (isPatched || !GLOBAL_OBJ.fetch) return;
|
|
141
104
|
|
|
142
|
-
if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patched__) {
|
|
143
105
|
GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
|
|
144
106
|
|
|
145
107
|
GLOBAL_OBJ.fetch = async function (input, init) {
|
|
146
108
|
const url = typeof input === 'string' ? input : (input && input.url);
|
|
147
109
|
|
|
148
|
-
//
|
|
149
|
-
if (url && url.includes(
|
|
110
|
+
// Guard: Don't track our own calls
|
|
111
|
+
if (url && url.includes(activeConfig.captureEndpoint)) {
|
|
150
112
|
return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
151
113
|
}
|
|
152
114
|
|
|
@@ -161,8 +123,6 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
161
123
|
if (!isAI) return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
162
124
|
|
|
163
125
|
const start = Date.now();
|
|
164
|
-
|
|
165
|
-
// --- Explicit Turn Increment Rule ---
|
|
166
126
|
let activeTurn;
|
|
167
127
|
if (lastTurnConfirmed) {
|
|
168
128
|
activeTurn = turnIndex++;
|
|
@@ -173,55 +133,47 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
173
133
|
|
|
174
134
|
const { provider, model } = normalize(url, init && init.body);
|
|
175
135
|
|
|
176
|
-
//
|
|
136
|
+
// Emit Request
|
|
177
137
|
let pText = "";
|
|
178
138
|
if (init && init.body) {
|
|
179
139
|
try { pText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
180
140
|
}
|
|
181
141
|
const pHash = hash(pText);
|
|
182
|
-
const isRetry = pHash && pHash === lastPromptHash;
|
|
183
142
|
|
|
184
|
-
|
|
143
|
+
emit({
|
|
185
144
|
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
186
145
|
turn_index: activeTurn,
|
|
187
146
|
direction: 'user_to_ai',
|
|
188
147
|
turn_role: 'user',
|
|
189
148
|
provider,
|
|
190
149
|
model,
|
|
191
|
-
|
|
150
|
+
content: activeConfig.privacyMode === 'full' ? pText : null,
|
|
192
151
|
content_hash: pHash,
|
|
193
|
-
metadata_flags: {
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
emit(requestEvent);
|
|
152
|
+
metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
|
|
153
|
+
});
|
|
199
154
|
lastPromptHash = pHash;
|
|
200
155
|
|
|
201
|
-
//
|
|
156
|
+
// Actual Call
|
|
202
157
|
let response;
|
|
203
158
|
let oText = "";
|
|
204
159
|
try {
|
|
205
160
|
response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
throw err;
|
|
208
|
-
}
|
|
161
|
+
} catch (err) { throw err; }
|
|
209
162
|
|
|
210
163
|
const latency = Date.now() - start;
|
|
211
164
|
|
|
212
|
-
//
|
|
165
|
+
// Emit Response
|
|
213
166
|
try {
|
|
214
167
|
const cloned = response.clone();
|
|
215
168
|
oText = await cloned.text();
|
|
216
|
-
if (oText && oText.length >
|
|
217
|
-
oText = oText.slice(0,
|
|
169
|
+
if (oText && oText.length > activeConfig.maxOutputBytes) {
|
|
170
|
+
oText = oText.slice(0, activeConfig.maxOutputBytes);
|
|
218
171
|
}
|
|
219
172
|
} catch (e) { }
|
|
220
173
|
|
|
221
174
|
const oHash = hash(oText);
|
|
222
|
-
const isNonAdaptive = oHash && oHash === lastResponseHash;
|
|
223
175
|
|
|
224
|
-
|
|
176
|
+
emit({
|
|
225
177
|
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
226
178
|
turn_index: activeTurn,
|
|
227
179
|
direction: 'ai_to_user',
|
|
@@ -229,98 +181,52 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
229
181
|
latency_ms: latency,
|
|
230
182
|
provider,
|
|
231
183
|
model,
|
|
232
|
-
|
|
184
|
+
content: activeConfig.privacyMode === 'full' ? oText : null,
|
|
233
185
|
content_hash: oHash,
|
|
234
186
|
metadata_flags: {
|
|
235
|
-
non_adaptive_response:
|
|
187
|
+
non_adaptive_response: oHash === lastResponseHash ? 1 : 0,
|
|
236
188
|
turn_boundary_confirmed: 1
|
|
237
189
|
}
|
|
238
|
-
};
|
|
190
|
+
});
|
|
239
191
|
|
|
240
|
-
emit(responseEvent);
|
|
241
192
|
lastResponseHash = oHash;
|
|
242
193
|
lastTurnConfirmed = true;
|
|
243
|
-
|
|
244
194
|
return response;
|
|
245
195
|
};
|
|
246
196
|
|
|
247
197
|
GLOBAL_OBJ.fetch.__dropout_patched__ = true;
|
|
198
|
+
isPatched = true;
|
|
248
199
|
}
|
|
249
200
|
|
|
250
|
-
|
|
251
|
-
* Manual capture for framework-level integration
|
|
252
|
-
*/
|
|
253
|
-
async function capture(target, options = {}) {
|
|
254
|
-
const start = Date.now();
|
|
255
|
-
|
|
256
|
-
let activeTurn;
|
|
257
|
-
if (lastTurnConfirmed) {
|
|
258
|
-
activeTurn = turnIndex++;
|
|
259
|
-
lastTurnConfirmed = false;
|
|
260
|
-
} else {
|
|
261
|
-
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
262
|
-
}
|
|
201
|
+
// --- MAIN CLASS EXPORT ---
|
|
263
202
|
|
|
264
|
-
|
|
203
|
+
class Dropout {
|
|
204
|
+
constructor(config = {}) {
|
|
205
|
+
if (instance) {
|
|
206
|
+
return instance;
|
|
207
|
+
}
|
|
265
208
|
|
|
266
|
-
|
|
209
|
+
if (!config.apiKey || !config.projectId) {
|
|
210
|
+
console.warn("[Dropout] Missing apiKey or projectId. Tracking disabled.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
267
213
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
latency_ms = Date.now() - start;
|
|
273
|
-
} else {
|
|
274
|
-
prompt = target.prompt;
|
|
275
|
-
output = target.output;
|
|
276
|
-
}
|
|
214
|
+
// Update Global Configuration
|
|
215
|
+
activeConfig.apiKey = config.apiKey;
|
|
216
|
+
activeConfig.projectId = config.projectId;
|
|
217
|
+
if (config.privacyMode) activeConfig.privacyMode = config.privacyMode;
|
|
277
218
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
emit({
|
|
283
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
284
|
-
turn_index: activeTurn,
|
|
285
|
-
direction: 'user_to_ai',
|
|
286
|
-
turn_role: 'user',
|
|
287
|
-
provider: options.provider || 'manual',
|
|
288
|
-
model: options.model || 'unknown',
|
|
289
|
-
content_raw: mode === 'full' ? prompt : null,
|
|
290
|
-
content_hash: pHash,
|
|
291
|
-
metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Emit Response
|
|
295
|
-
emit({
|
|
296
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
297
|
-
turn_index: activeTurn,
|
|
298
|
-
direction: 'ai_to_user',
|
|
299
|
-
turn_role: 'assistant',
|
|
300
|
-
latency_ms,
|
|
301
|
-
provider: options.provider || 'manual',
|
|
302
|
-
model: options.model || 'unknown',
|
|
303
|
-
content_raw: mode === 'full' ? output : null,
|
|
304
|
-
content_hash: oHash,
|
|
305
|
-
metadata_flags: { non_adaptive_response: oHash === lastResponseHash ? 1 : 0, turn_boundary_confirmed: 1 }
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
lastPromptHash = pHash;
|
|
309
|
-
lastResponseHash = oHash;
|
|
310
|
-
lastTurnConfirmed = true;
|
|
311
|
-
|
|
312
|
-
return output;
|
|
313
|
-
}
|
|
219
|
+
// Initialize Identity
|
|
220
|
+
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
221
|
+
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
222
|
+
}
|
|
314
223
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
GLOBAL_OBJ.__dropout_session_ended__ = false;
|
|
321
|
-
turnIndex = 0;
|
|
322
|
-
lastTurnConfirmed = true;
|
|
323
|
-
lastPromptHash = null;
|
|
324
|
-
lastResponseHash = null;
|
|
224
|
+
// Apply the patch
|
|
225
|
+
applyPatch();
|
|
226
|
+
|
|
227
|
+
console.log("[Dropout] Initialized for project:", config.projectId);
|
|
228
|
+
instance = this;
|
|
325
229
|
}
|
|
326
|
-
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = Dropout;
|