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