@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +95 -189
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.2.13",
3
+ "version": "0.3.0",
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
@@ -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: None (Public Endpoint).
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
- // --- Identity & State ---
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
- function generateSessionId() {
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
- let config = {
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
- console.log("[Dropout Debug] Runtime Initializing...");
41
- console.log("[Dropout Debug] Session ID:", GLOBAL_OBJ.__dropout_session_id__ || "Not set");
42
- /**
43
- * Telemetry Emitter (Non-blocking, Fire-and-forget)
44
- */
45
- function emit(payload) {
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
- // --- The Monkey Patch ---
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
- // GUARD: Avoid infinite loops
149
- if (url && url.includes(config.captureEndpoint)) {
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
- // --- 1. Emit Request Event ---
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
- const requestEvent = {
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
- content_raw: config.privacyMode === 'full' ? pText : null,
150
+ content: activeConfig.privacyMode === 'full' ? pText : null,
192
151
  content_hash: pHash,
193
- metadata_flags: {
194
- retry_like: isRetry ? 1 : 0
195
- }
196
- };
197
-
198
- emit(requestEvent);
152
+ metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
153
+ });
199
154
  lastPromptHash = pHash;
200
155
 
201
- // Execute actual fetch
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
- // --- 2. Emit Response Event ---
165
+ // Emit Response
213
166
  try {
214
167
  const cloned = response.clone();
215
168
  oText = await cloned.text();
216
- if (oText && oText.length > config.maxOutputBytes) {
217
- oText = oText.slice(0, config.maxOutputBytes);
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
- const responseEvent = {
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
- content_raw: config.privacyMode === 'full' ? oText : null,
184
+ content: activeConfig.privacyMode === 'full' ? oText : null,
233
185
  content_hash: oHash,
234
186
  metadata_flags: {
235
- non_adaptive_response: isNonAdaptive ? 1 : 0,
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
- const mode = options.privacy || config.privacyMode;
203
+ class Dropout {
204
+ constructor(config = {}) {
205
+ if (instance) {
206
+ return instance;
207
+ }
265
208
 
266
- let prompt, output, latency_ms = options.latency || 0;
209
+ if (!config.apiKey || !config.projectId) {
210
+ console.warn("[Dropout] Missing apiKey or projectId. Tracking disabled.");
211
+ return;
212
+ }
267
213
 
268
- if (typeof target === 'function' || (target && typeof target.then === 'function')) {
269
- prompt = options.prompt;
270
- const result = typeof target === 'function' ? await target() : await target;
271
- output = typeof result === 'string' ? result : JSON.stringify(result);
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
- const pHash = hash(prompt);
279
- const oHash = hash(output);
280
-
281
- // Emit Request
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
- module.exports = {
316
- capture,
317
- reset: (reason = 'manual_reset') => {
318
- emitSessionEnd(reason);
319
- GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
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;