sovergate 0.1.0-alpha.2 → 0.1.0-alpha.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/README.md CHANGED
@@ -59,12 +59,47 @@ init({ apiKey: 'sov_prod_xxxx' })
59
59
  instrument(client)
60
60
  ```
61
61
 
62
+ ### Streaming
63
+
64
+ Streamed calls are logged too: `create({ stream: true })` on both providers, plus
65
+ the `chat.completions.stream()` and `messages.stream()` helpers. Your app gets
66
+ the same stream object and every chunk unchanged as soon as it arrives; the SDK
67
+ keeps a copy of the text and logs it once the stream ends. If your app stops
68
+ reading part-way, what it had received up to that point is logged.
69
+
70
+ OpenAI only reports token counts for a stream when you ask for them:
71
+
72
+ ```ts
73
+ client.chat.completions.create({ ..., stream: true, stream_options: { include_usage: true } })
74
+ ```
75
+
76
+ Without that option the record is still logged, with 0 tokens. The SDK never
77
+ adds the option itself, because it changes the chunks your app receives.
78
+
79
+ ## Stable PII tokens (recommended)
80
+
81
+ By default, detected PII is replaced with positional placeholders
82
+ (`[EMAIL_REDACTED_1]`), which are not correlatable between logs. Pass a
83
+ `piiKey` to replace values with deterministic, irreversible HMAC-SHA256 tokens
84
+ instead. The same value always maps to the same token, so Sovergate can count
85
+ unique people, IBANs and so on without ever receiving the raw values.
86
+
87
+ ```typescript
88
+ init({ apiKey: 'sov_prod_xxxx', piiKey: process.env.SOVERGATE_PII_KEY })
89
+ ```
90
+
91
+ The `piiKey` is your secret. Keep it stable, and never send it to us: it stays
92
+ on your infrastructure, and without it the tokens cannot be reversed. The Python
93
+ SDK produces the same tokens for the same key, so services in both languages
94
+ count a person once.
95
+
62
96
  ## Options
63
97
 
64
98
  ```typescript
65
99
  init({
66
100
  apiKey: 'sov_prod_xxxx', // required
67
101
  baseUrl: 'https://...', // default: https://api.sovergate.com
102
+ piiKey: '<your-secret>', // stable PII tokens (see above)
68
103
  debug: true, // print SDK internals to console.debug
69
104
  })
70
105
  ```
@@ -97,9 +132,12 @@ mind when you build prompts. The code is in [`src/scrubber.ts`](src/scrubber.ts)
97
132
  ## Guarantees
98
133
 
99
134
  - Never blocks the event loop: logging runs via `setImmediate`
135
+ - Never keeps your process alive: a script or job exits normally even while
136
+ records are waiting to be retried
100
137
  - Never throws: if setup fails, the SDK logs a `console.warn` and stays out of the way
101
138
  - Leaves your client unchanged: `create()` still returns the provider SDK's own
102
- object, so `.withResponse()` and `.asResponse()` keep working
139
+ object, so `.withResponse()` and `.asResponse()` keep working, and streams keep
140
+ their `.controller`, `.tee()` and `.toReadableStream()`
103
141
  - 2 second timeout on every HTTP call to Sovergate
104
142
  - If Sovergate is unreachable, records are queued in memory (up to 10,000) and
105
143
  retried with exponential backoff
@@ -108,8 +146,7 @@ mind when you build prompts. The code is in [`src/scrubber.ts`](src/scrubber.ts)
108
146
 
109
147
  ## Current limitations
110
148
 
111
- - Streaming calls (`stream: true`) are passed through untouched but not logged yet.
112
- - PII placeholders are positional (`[EMAIL_REDACTED_1]`) and not correlatable across logs.
149
+ - Only the text of a response is logged, streamed or not; tool calls are not.
113
150
  - The retry queue is in memory, so queued records are lost if the process exits.
114
151
 
115
152
  ## Development
package/dist/index.d.mts CHANGED
@@ -1,9 +1,16 @@
1
1
  interface InitOptions {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
+ /**
5
+ * Secret used to turn detected personal data into stable, irreversible tokens.
6
+ * The same value always maps to the same token, so Sovergate can count unique
7
+ * people without holding the data. Keep it secret and stable: it never leaves
8
+ * your infrastructure. Without it, placeholders are numbered per record.
9
+ */
10
+ piiKey?: string;
4
11
  debug?: boolean;
5
12
  }
6
- declare function init({ apiKey, baseUrl, debug }: InitOptions): void;
13
+ declare function init({ apiKey, baseUrl, piiKey, debug }: InitOptions): void;
7
14
  declare function instrument(client: any): void;
8
15
  declare function shutdown(): void;
9
16
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  interface InitOptions {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
+ /**
5
+ * Secret used to turn detected personal data into stable, irreversible tokens.
6
+ * The same value always maps to the same token, so Sovergate can count unique
7
+ * people without holding the data. Keep it secret and stable: it never leaves
8
+ * your infrastructure. Without it, placeholders are numbered per record.
9
+ */
10
+ piiKey?: string;
4
11
  debug?: boolean;
5
12
  }
6
- declare function init({ apiKey, baseUrl, debug }: InitOptions): void;
13
+ declare function init({ apiKey, baseUrl, piiKey, debug }: InitOptions): void;
7
14
  declare function instrument(client: any): void;
8
15
  declare function shutdown(): void;
9
16
 
package/dist/index.js CHANGED
@@ -31,9 +31,11 @@ var BASE_URL = "https://api.sovergate.com";
31
31
  var TIMEOUT_MS = 2e3;
32
32
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
33
33
  async function send(apiKey, payload, baseUrl = BASE_URL) {
34
+ let timer;
34
35
  try {
35
36
  const controller = new AbortController();
36
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
37
+ timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
38
+ timer.unref?.();
37
39
  const response = await fetch(`${baseUrl}/ingest`, {
38
40
  method: "POST",
39
41
  headers: {
@@ -43,7 +45,6 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
43
45
  body: JSON.stringify(payload),
44
46
  signal: controller.signal
45
47
  });
46
- clearTimeout(timer);
47
48
  if (response.status < 400) return "sent";
48
49
  if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return "retry";
49
50
  try {
@@ -53,6 +54,8 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
53
54
  return "drop";
54
55
  } catch {
55
56
  return "retry";
57
+ } finally {
58
+ clearTimeout(timer);
56
59
  }
57
60
  }
58
61
 
@@ -60,7 +63,10 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
60
63
  var MAX_QUEUE = 1e4;
61
64
  var RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60];
62
65
  function sleep(ms) {
63
- return new Promise((resolve) => setTimeout(resolve, ms));
66
+ return new Promise((resolve) => {
67
+ const timer = setTimeout(resolve, ms);
68
+ timer.unref?.();
69
+ });
64
70
  }
65
71
  var LogQueue = class {
66
72
  sendFn;
@@ -100,21 +106,41 @@ var LogQueue = class {
100
106
  };
101
107
 
102
108
  // src/scrubber.ts
109
+ var import_node_crypto = require("crypto");
110
+ var TOKEN_HEX_LEN = 12;
103
111
  var RULES = [
104
112
  ["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
113
+ // IP before phone: the phone rule would otherwise swallow dotted quads
105
114
  ["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
106
- ["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
115
+ // IBAN: tolerate the usual 4-character grouping ("DE89 3704 0044 ...") so a spaced
116
+ // IBAN can't leak its digits to the phone rule
117
+ ["iban", /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}\b/g, "IBAN_REDACTED"],
118
+ // Card numbers before phone, so the whole grouped number (13–20 digits) is caught.
119
+ // Reported as id_number: the ingest API has no card category.
120
+ ["id_number", /\b\d{4}(?:[ -]?\d{4}){2,4}\b/g, "CARD_REDACTED"],
107
121
  // (?<![\w+]) instead of \b so a leading "+" is part of the match, not left behind
108
- ["phone", /(?<![\w+])\+?\d[\d\s\-(). ]{5,14}\d\b/g, "PHONE_REDACTED"],
122
+ ["phone", /(?<![\w+])\+?\d[\d\s\-().]{5,14}\d\b/g, "PHONE_REDACTED"],
109
123
  ["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
110
124
  ["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
125
+ // Names last: a broad heuristic, applied only after a title
111
126
  ["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
112
127
  ];
128
+ function normalize(value) {
129
+ return value.replace(/[\s-]/g, "").toLowerCase();
130
+ }
113
131
  var ScrubContext = class {
132
+ constructor(piiKey) {
133
+ this.piiKey = piiKey;
134
+ }
135
+ piiKey;
114
136
  seen = /* @__PURE__ */ new Map();
115
137
  next = /* @__PURE__ */ new Map();
116
138
  placeholder(base, value) {
117
- const key = `${base}\0${value.replace(/[\s-]/g, "").toLowerCase()}`;
139
+ if (this.piiKey) {
140
+ const digest = (0, import_node_crypto.createHmac)("sha256", this.piiKey).update(normalize(value), "utf8").digest("hex");
141
+ return `[${base}_${digest.slice(0, TOKEN_HEX_LEN)}]`;
142
+ }
143
+ const key = `${base}\0${normalize(value)}`;
118
144
  let n = this.seen.get(key);
119
145
  if (n === void 0) {
120
146
  n = (this.next.get(base) ?? 0) + 1;
@@ -182,9 +208,15 @@ function observe(result, onData) {
182
208
  };
183
209
  try {
184
210
  return new Proxy(result, {
185
- get(target, prop) {
211
+ get(target, prop, proxy) {
186
212
  const value = Reflect.get(target, prop, target);
187
213
  if (typeof value !== "function") return value;
214
+ if (prop === "catch" || prop === "finally") {
215
+ return (...args) => (
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ Promise.prototype[prop].apply(proxy, args)
218
+ );
219
+ }
188
220
  if (prop === "then") {
189
221
  return (onFulfilled, onRejected) => value.call(
190
222
  target,
@@ -215,102 +247,200 @@ function observe(result, onData) {
215
247
  function isStream(opts) {
216
248
  return opts?.stream === true;
217
249
  }
218
- function patchOpenAI(client, enqueue) {
219
- const completions = client.chat.completions;
220
- if (completions[PATCHED]) return;
221
- const original = completions.create.bind(completions);
222
- completions.create = (opts, ...rest) => {
223
- const requestAt = utcnow();
224
- const t0 = performance.now();
225
- const result = original(opts, ...rest);
226
- if (isStream(opts)) return result;
227
- return observe(result, (response) => {
228
- const latencyMs = Math.round(performance.now() - t0);
229
- setImmediate(() => {
250
+ function safely(fn) {
251
+ try {
252
+ fn();
253
+ } catch {
254
+ }
255
+ }
256
+ function instrumentStream(stream, collector, done) {
257
+ if (!stream || typeof stream[Symbol.asyncIterator] !== "function") return;
258
+ const iterate = stream[Symbol.asyncIterator];
259
+ let finished = false;
260
+ const finish = () => {
261
+ if (finished) return;
262
+ finished = true;
263
+ safely(() => {
264
+ const response = collector.response();
265
+ if (response) done(response);
266
+ });
267
+ };
268
+ stream[Symbol.asyncIterator] = function() {
269
+ const iterator = iterate.call(this);
270
+ return {
271
+ async next(...args) {
272
+ let result;
230
273
  try {
231
- const messages = opts?.messages ?? [];
232
- const model = opts?.model ?? "";
233
- const context = new ScrubContext();
234
- const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
235
- const choice = response?.choices?.[0];
236
- const responseText = choice?.message?.content ?? "";
237
- const scrubbedResult = responseText ? scrub(responseText, context) : null;
238
- const scrubbedResponse = scrubbedResult?.text ?? "";
239
- const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
240
- const usage = response?.usage;
241
- enqueue({
242
- model: response?.model ?? model,
243
- messages: scrubbedMessages,
244
- response: scrubbedResponse,
245
- prompt_tokens: usage?.prompt_tokens ?? 0,
246
- completion_tokens: usage?.completion_tokens ?? 0,
247
- latency_ms: latencyMs,
248
- timestamp: requestAt,
249
- finish_reason: choice?.finish_reason ?? null,
250
- pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
251
- });
252
- } catch {
274
+ result = await iterator.next(...args);
275
+ } catch (err) {
276
+ finish();
277
+ throw err;
253
278
  }
254
- });
255
- });
279
+ if (result.done) finish();
280
+ else safely(() => collector.add(result.value));
281
+ return result;
282
+ },
283
+ // `break` out of a for await loop lands here.
284
+ async return(value) {
285
+ finish();
286
+ return typeof iterator.return === "function" ? iterator.return(value) : { done: true, value };
287
+ },
288
+ async throw(err) {
289
+ finish();
290
+ if (typeof iterator.throw === "function") return iterator.throw(err);
291
+ throw err;
292
+ },
293
+ [Symbol.asyncIterator]() {
294
+ return this;
295
+ }
296
+ };
297
+ };
298
+ }
299
+ function openAIStreamCollector() {
300
+ let seen = false;
301
+ let model;
302
+ let finishReason = null;
303
+ let usage;
304
+ const parts = [];
305
+ return {
306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
+ add(chunk) {
308
+ seen = true;
309
+ model = chunk?.model ?? model;
310
+ usage = chunk?.usage ?? usage;
311
+ for (const choice of chunk?.choices ?? []) {
312
+ if ((choice?.index ?? 0) !== 0) continue;
313
+ if (choice?.delta?.content) parts.push(choice.delta.content);
314
+ finishReason = choice?.finish_reason ?? finishReason;
315
+ }
316
+ },
317
+ response() {
318
+ if (!seen) return null;
319
+ return { model, choices: [{ message: { content: parts.join("") }, finish_reason: finishReason }], usage };
320
+ }
321
+ };
322
+ }
323
+ function anthropicStreamCollector() {
324
+ let seen = false;
325
+ let model;
326
+ let stopReason = null;
327
+ let inputTokens = 0;
328
+ let outputTokens = 0;
329
+ const parts = [];
330
+ return {
331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
332
+ add(event) {
333
+ seen = true;
334
+ if (event?.type === "message_start") {
335
+ model = event.message?.model;
336
+ inputTokens = event.message?.usage?.input_tokens ?? 0;
337
+ outputTokens = event.message?.usage?.output_tokens ?? 0;
338
+ } else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
339
+ parts.push(event.delta.text);
340
+ } else if (event?.type === "message_delta") {
341
+ stopReason = event.delta?.stop_reason ?? stopReason;
342
+ outputTokens = event.usage?.output_tokens ?? outputTokens;
343
+ inputTokens = event.usage?.input_tokens ?? inputTokens;
344
+ }
345
+ },
346
+ response() {
347
+ if (!seen) return null;
348
+ return {
349
+ model,
350
+ content: [{ type: "text", text: parts.join("") }],
351
+ stop_reason: stopReason,
352
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens }
353
+ };
354
+ }
256
355
  };
257
- completions[PATCHED] = true;
258
356
  }
259
- function patchAnthropic(client, enqueue) {
260
- const messagesApi = client.messages;
261
- if (messagesApi[PATCHED]) return;
262
- const original = messagesApi.create.bind(messagesApi);
263
- messagesApi.create = (opts, ...rest) => {
357
+ function wrapCreate(target, enqueue, build, collector, piiKey) {
358
+ if (target[PATCHED]) return;
359
+ const original = target.create.bind(target);
360
+ target.create = (opts, ...rest) => {
264
361
  const requestAt = utcnow();
265
362
  const t0 = performance.now();
266
363
  const result = original(opts, ...rest);
267
- if (isStream(opts)) return result;
268
- return observe(result, (response) => {
364
+ const done = (response) => {
269
365
  const latencyMs = Math.round(performance.now() - t0);
270
- setImmediate(() => {
271
- try {
272
- const messages = opts?.messages ?? [];
273
- const system = opts?.system;
274
- const model = opts?.model ?? "";
275
- const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
276
- const context = new ScrubContext();
277
- const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
278
- const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
279
- const scrubbedResult = responseText ? scrub(responseText, context) : null;
280
- const scrubbedResponse = scrubbedResult?.text ?? "";
281
- const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
282
- const usage = response?.usage;
283
- enqueue({
284
- model: response?.model ?? model,
285
- messages: scrubbedMessages,
286
- response: scrubbedResponse,
287
- prompt_tokens: usage?.input_tokens ?? 0,
288
- completion_tokens: usage?.output_tokens ?? 0,
289
- latency_ms: latencyMs,
290
- timestamp: requestAt,
291
- finish_reason: response?.stop_reason ?? null,
292
- pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
293
- });
294
- } catch {
295
- }
296
- });
297
- });
366
+ setImmediate(() => safely(() => enqueue(build(opts, response, requestAt, latencyMs, piiKey))));
367
+ };
368
+ if (isStream(opts)) return observe(result, (stream) => instrumentStream(stream, collector(), done));
369
+ return observe(result, done);
370
+ };
371
+ target[PATCHED] = true;
372
+ }
373
+ function buildOpenAIPayload(opts, response, requestAt, latencyMs, piiKey) {
374
+ const messages = opts?.messages ?? [];
375
+ const model = opts?.model ?? "";
376
+ const context = new ScrubContext(piiKey);
377
+ const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
378
+ const choice = response?.choices?.[0];
379
+ const responseText = choice?.message?.content ?? "";
380
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
381
+ const scrubbedResponse = scrubbedResult?.text ?? "";
382
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
383
+ const usage = response?.usage;
384
+ return {
385
+ model: response?.model ?? model,
386
+ messages: scrubbedMessages,
387
+ response: scrubbedResponse,
388
+ prompt_tokens: usage?.prompt_tokens ?? 0,
389
+ completion_tokens: usage?.completion_tokens ?? 0,
390
+ latency_ms: latencyMs,
391
+ timestamp: requestAt,
392
+ finish_reason: choice?.finish_reason ?? null,
393
+ pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
298
394
  };
299
- messagesApi[PATCHED] = true;
395
+ }
396
+ function buildAnthropicPayload(opts, response, requestAt, latencyMs, piiKey) {
397
+ const messages = opts?.messages ?? [];
398
+ const system = opts?.system;
399
+ const model = opts?.model ?? "";
400
+ const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
401
+ const context = new ScrubContext(piiKey);
402
+ const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
403
+ const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
404
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
405
+ const scrubbedResponse = scrubbedResult?.text ?? "";
406
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
407
+ const usage = response?.usage;
408
+ return {
409
+ model: response?.model ?? model,
410
+ messages: scrubbedMessages,
411
+ response: scrubbedResponse,
412
+ prompt_tokens: usage?.input_tokens ?? 0,
413
+ completion_tokens: usage?.output_tokens ?? 0,
414
+ latency_ms: latencyMs,
415
+ timestamp: requestAt,
416
+ finish_reason: response?.stop_reason ?? null,
417
+ pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
418
+ };
419
+ }
420
+ function patchOpenAI(client, enqueue, piiKey) {
421
+ wrapCreate(client.chat.completions, enqueue, buildOpenAIPayload, openAIStreamCollector, piiKey);
422
+ }
423
+ function patchAnthropic(client, enqueue, piiKey) {
424
+ wrapCreate(client.messages, enqueue, buildAnthropicPayload, anthropicStreamCollector, piiKey);
300
425
  }
301
426
 
302
427
  // src/client.ts
303
428
  var state = {
304
429
  apiKey: null,
305
430
  baseUrl: "https://api.sovergate.com",
431
+ piiKey: void 0,
306
432
  queue: null
307
433
  };
308
- function init({ apiKey, baseUrl = "https://api.sovergate.com", debug = false }) {
434
+ function init({ apiKey, baseUrl = "https://api.sovergate.com", piiKey, debug = false }) {
309
435
  try {
310
436
  state.apiKey = apiKey;
311
437
  state.baseUrl = baseUrl.replace(/\/$/, "");
438
+ state.piiKey = piiKey || void 0;
312
439
  if (debug) {
313
440
  console.debug("[sovergate] init baseUrl=%s", state.baseUrl);
441
+ if (!state.piiKey) {
442
+ console.debug("[sovergate] no piiKey set: PII placeholders are numbered per record and not correlatable across logs");
443
+ }
314
444
  }
315
445
  state.queue = new LogQueue((payload) => send(apiKey, payload, state.baseUrl));
316
446
  } catch (err) {
@@ -326,11 +456,11 @@ function instrument(client) {
326
456
  try {
327
457
  const enqueue = state.queue.enqueue.bind(state.queue);
328
458
  if (client?.chat?.completions) {
329
- patchOpenAI(client, enqueue);
459
+ patchOpenAI(client, enqueue, state.piiKey);
330
460
  return;
331
461
  }
332
462
  if (client?.messages) {
333
- patchAnthropic(client, enqueue);
463
+ patchAnthropic(client, enqueue, state.piiKey);
334
464
  return;
335
465
  }
336
466
  warn(
@@ -348,6 +478,7 @@ function warn(message, err) {
348
478
  }
349
479
  function shutdown() {
350
480
  state.queue = null;
481
+ state.piiKey = void 0;
351
482
  }
352
483
  // Annotate the CommonJS export names for ESM import in node:
353
484
  0 && (module.exports = {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["export { init, instrument, shutdown } from './client'\nexport type { InitOptions } from './client'\nexport type { Detection, ScrubResult } from './scrubber'\n","const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\nconst RETRYABLE_STATUS = new Set([408, 425, 429])\n\n/**\n * sent: accepted by the server\n * retry: temporary failure, keep the record and try again later\n * drop: permanently rejected, retrying the same record can never succeed\n */\nexport type SendOutcome = 'sent' | 'retry' | 'drop'\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<SendOutcome> {\n try {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n if (response.status < 400) return 'sent'\n if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return 'retry'\n\n // 400 (invalid record), 401/403 (bad or revoked key), 413 (too large)...\n // Retrying would block every later record behind this one.\n try {\n console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`)\n } catch {\n // never propagate\n }\n return 'drop'\n } catch {\n return 'retry'\n }\n}\n","import type { SendOutcome } from './sender'\n\ntype Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<SendOutcome | boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let done = false\n try {\n const outcome = await this.sendFn(this.items[0])\n // 'sent' and 'drop' both finish this record; true is accepted as 'sent'.\n done = outcome === true || outcome === 'sent' || outcome === 'drop'\n } catch {\n // never propagate\n }\n\n if (done) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","export interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Each rule: [category, regex (must have g flag), placeholder base]\n// Order matters — IP before phone to avoid dotted-quad false matches.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n ['iban', /\\b[A-Z]{2}\\d{2}[A-Z0-9]{4,30}\\b/g, 'IBAN_REDACTED'],\n // (?<![\\w+]) instead of \\b so a leading \"+\" is part of the match, not left behind\n ['phone', /(?<![\\w+])\\+?\\d[\\d\\s\\-(). ]{5,14}\\d\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\n/**\n * Numbers placeholders across one whole record (all messages and the response),\n * so the same value always gets the same placeholder and two different values\n * never share one.\n */\nexport class ScrubContext {\n private readonly seen = new Map<string, number>()\n private readonly next = new Map<string, number>()\n\n placeholder(base: string, value: string): string {\n const key = `${base}\\u0000${value.replace(/[\\s-]/g, '').toLowerCase()}`\n let n = this.seen.get(key)\n if (n === undefined) {\n n = (this.next.get(base) ?? 0) + 1\n this.next.set(base, n)\n this.seen.set(key, n)\n }\n return `[${base}_${n}]`\n }\n}\n\nexport function scrub(text: string, context: ScrubContext = new ScrubContext()): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, match => {\n const placeholder = context.placeholder(basePlaceholder, match)\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(\n messages: OpenAIMessage[],\n context: ScrubContext = new ScrubContext(),\n): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content, context)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text, context)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { ScrubContext, scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue): void {\n const completions = client.chat.completions\n if (completions[PATCHED]) return\n const original = completions.create.bind(completions)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n completions.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const context = new ScrubContext()\n const [scrubbedMessages, promptDetections] = scrubMessages(messages, context)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n completions[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue): void {\n const messagesApi = client.messages\n if (messagesApi[PATCHED]) return\n const original = messagesApi.create.bind(messagesApi)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messagesApi.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const context = new ScrubContext()\n const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n messagesApi[PATCHED] = true\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAM,WAAW;AACjB,IAAM,aAAa;AACnB,IAAM,mBAAmB,oBAAI,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC;AAShD,eAAsB,KACpB,QACA,SACA,UAAU,UACY;AACtB,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,UAAU;AAE7D,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,WAAW;AAAA,MAChD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,MAAM;AAAA,QAC/B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,KAAK;AAClB,QAAI,SAAS,SAAS,IAAK,QAAO;AAClC,QAAI,iBAAiB,IAAI,SAAS,MAAM,KAAK,SAAS,UAAU,IAAK,QAAO;AAI5E,QAAI;AACF,cAAQ,KAAK,8CAA8C,SAAS,MAAM,gBAAgB;AAAA,IAC5F,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACxCA,IAAM,YAAY;AAClB,IAAM,iBAAiB,CAAC,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE;AAE9C,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;AAEO,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA,QAAmB,CAAC;AAAA,EAC7B,WAAW;AAAA,EAEnB,YAAY,QAAgB;AAC1B,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,QAAQ,SAAwB;AAC9B,QAAI,KAAK,MAAM,UAAU,UAAW;AACpC,SAAK,MAAM,KAAK,OAAO;AACvB,QAAI,CAAC,KAAK,UAAU;AAClB,mBAAa,MAAM,KAAK,KAAK,MAAM,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,MAAc,QAAuB;AACnC,SAAK,WAAW;AAChB,QAAI,aAAa;AAEjB,WAAO,KAAK,MAAM,SAAS,GAAG;AAC5B,UAAI,OAAO;AACX,UAAI;AACF,cAAM,UAAU,MAAM,KAAK,OAAO,KAAK,MAAM,CAAC,CAAC;AAE/C,eAAO,YAAY,QAAQ,YAAY,UAAU,YAAY;AAAA,MAC/D,QAAQ;AAAA,MAER;AAEA,UAAI,MAAM;AACR,aAAK,MAAM,MAAM;AACjB,qBAAa;AAAA,MACf,OAAO;AACL,cAAM,WAAW,eAAe,KAAK,IAAI,YAAY,eAAe,SAAS,CAAC,CAAC;AAC/E;AACA,cAAM,MAAM,WAAW,GAAI;AAAA,MAC7B;AAAA,IACF;AAEA,SAAK,WAAW;AAAA,EAClB;AACF;;;ACxCA,IAAM,QAAyC;AAAA,EAC7C,CAAC,SAAa,yDAAsF,gBAAgB;AAAA,EACpH,CAAC,MAAa,gCAAuF,aAAa;AAAA,EAClH,CAAC,QAAa,oCAAuF,eAAe;AAAA;AAAA,EAEpH,CAAC,SAAa,0CAAqF,gBAAgB;AAAA,EACnH,CAAC,OAAa,kFAAoF,cAAc;AAAA,EAChH,CAAC,aAAa,0BAAuF,aAAa;AAAA,EAClH,CAAC,QAAa,gEAAsF,eAAe;AACrH;AAOO,IAAM,eAAN,MAAmB;AAAA,EACP,OAAO,oBAAI,IAAoB;AAAA,EAC/B,OAAO,oBAAI,IAAoB;AAAA,EAEhD,YAAY,MAAc,OAAuB;AAC/C,UAAM,MAAM,GAAG,IAAI,KAAS,MAAM,QAAQ,UAAU,EAAE,EAAE,YAAY,CAAC;AACrE,QAAI,IAAI,KAAK,KAAK,IAAI,GAAG;AACzB,QAAI,MAAM,QAAW;AACnB,WAAK,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK;AACjC,WAAK,KAAK,IAAI,MAAM,CAAC;AACrB,WAAK,KAAK,IAAI,KAAK,CAAC;AAAA,IACtB;AACA,WAAO,IAAI,IAAI,IAAI,CAAC;AAAA,EACtB;AACF;AAEO,SAAS,MAAM,MAAc,UAAwB,IAAI,aAAa,GAAgB;AAC3F,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,YAAY,CAAC,EAAE;AAEzC,QAAM,aAA0B,CAAC;AAEjC,MAAI,SAAS;AACb,aAAW,CAAC,UAAU,SAAS,eAAe,KAAK,OAAO;AACxD,YAAQ,YAAY;AACpB,aAAS,OAAO,QAAQ,SAAS,WAAS;AACxC,YAAM,cAAc,QAAQ,YAAY,iBAAiB,KAAK;AAC9D,iBAAW,KAAK,EAAE,UAAU,YAAY,CAAC;AACzC,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,MAAM,QAAQ,WAAW;AACpC;AAEO,SAAS,cACd,UACA,UAAwB,IAAI,aAAa,GACT;AAChC,QAAM,gBAA6B,CAAC;AACpC,QAAM,WAA4B,CAAC;AAEnC,aAAW,OAAO,UAAU;AAC1B,UAAM,UAAU,IAAI;AACpB,QAAI,OAAO,YAAY,UAAU;AAC/B,YAAM,SAAS,MAAM,SAAS,OAAO;AACrC,eAAS,KAAK,EAAE,GAAG,KAAK,SAAS,OAAO,KAAK,CAAC;AAC9C,oBAAc,KAAK,GAAG,OAAO,UAAU;AAAA,IACzC,WAAW,MAAM,QAAQ,OAAO,GAAG;AACjC,YAAM,YAAY,QAAQ,IAAI,WAAS;AACrC,YAAI,MAAM,SAAS,UAAU,OAAO,MAAM,SAAS,UAAU;AAC3D,gBAAM,SAAS,MAAM,MAAM,MAAM,OAAO;AACxC,wBAAc,KAAK,GAAG,OAAO,UAAU;AACvC,iBAAO,EAAE,GAAG,OAAO,MAAM,OAAO,KAAK;AAAA,QACvC;AACA,eAAO;AAAA,MACT,CAAC;AACD,eAAS,KAAK,EAAE,GAAG,KAAK,SAAS,UAAU,CAAC;AAAA,IAC9C,OAAO;AACL,eAAS,KAAK,GAAG;AAAA,IACnB;AAAA,EACF;AAEA,SAAO,CAAC,UAAU,aAAa;AACjC;;;AC1FA,IAAM,UAAU,uBAAO,IAAI,mBAAmB;AAE9C,SAAS,SAAiB;AACxB,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAaA,SAAS,QAAQ,QAAa,QAAkC;AAC9D,MAAI,CAAC,UAAU,OAAO,OAAO,SAAS,WAAY,QAAO;AAEzD,MAAI,OAAO;AAEX,QAAM,SAAS,CAAC,SAAoB;AAClC,QAAI,KAAM;AACV,WAAO;AACP,QAAI;AACF,aAAO,IAAI;AAAA,IACb,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI;AACF,WAAO,IAAI,MAAM,QAAQ;AAAA,MACvB,IAAI,QAAQ,MAAM;AAChB,cAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,YAAI,OAAO,UAAU,WAAY,QAAO;AAExC,YAAI,SAAS,QAAQ;AAEnB,iBAAO,CAAC,aAAmB,eACzB,MAAM;AAAA,YACJ;AAAA;AAAA,YAEA,CAAC,SAAc;AACb,qBAAO,IAAI;AACX,qBAAO,OAAO,gBAAgB,aAAa,YAAY,IAAI,IAAI;AAAA,YACjE;AAAA,YACA;AAAA,UACF;AAAA,QACJ;AAEA,YAAI,SAAS,gBAAgB;AAC3B,iBAAO,IAAI;AAAA;AAAA,YAET,MAAM,MAAM,QAAQ,IAAI,EAAE,KAAK,CAAC,iBAAsB;AACpD,qBAAO,cAAc,IAAI;AACzB,qBAAO;AAAA,YACT,CAAC;AAAA;AAAA,QACL;AAEA,eAAO,MAAM,KAAK,MAAM;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,SAAS,MAAoB;AACpC,SAAO,MAAM,WAAW;AAC1B;AAGO,SAAS,YAAY,QAAa,SAAwB;AAC/D,QAAM,cAAc,OAAO,KAAK;AAChC,MAAI,YAAY,OAAO,EAAG;AAC1B,QAAM,WAAW,YAAY,OAAO,KAAK,WAAW;AAGpD,cAAY,SAAS,CAAC,SAAc,SAAoB;AACtD,UAAM,YAAY,OAAO;AACzB,UAAM,KAAK,YAAY,IAAI;AAE3B,UAAM,SAAS,SAAS,MAAM,GAAG,IAAI;AAGrC,QAAI,SAAS,IAAI,EAAG,QAAO;AAG3B,WAAO,QAAQ,QAAQ,CAAC,aAAkB;AACxC,YAAM,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,EAAE;AAEnD,mBAAa,MAAM;AACjB,YAAI;AACF,gBAAM,WAAW,MAAM,YAAY,CAAC;AACpC,gBAAM,QAAQ,MAAM,SAAS;AAC7B,gBAAM,UAAU,IAAI,aAAa;AACjC,gBAAM,CAAC,kBAAkB,gBAAgB,IAAI,cAAc,UAAU,OAAO;AAC5E,gBAAM,SAAS,UAAU,UAAU,CAAC;AACpC,gBAAM,eAAuB,QAAQ,SAAS,WAAW;AACzD,gBAAM,iBAAiB,eAAe,MAAM,cAAc,OAAO,IAAI;AACrE,gBAAM,mBAAmB,gBAAgB,QAAQ;AACjD,gBAAM,aAAa,CAAC,GAAG,kBAAkB,GAAI,gBAAgB,cAAc,CAAC,CAAE;AAC9E,gBAAM,QAAQ,UAAU;AAExB,kBAAQ;AAAA,YACN,OAAO,UAAU,SAAS;AAAA,YAC1B,UAAU;AAAA,YACV,UAAU;AAAA,YACV,eAAe,OAAO,iBAAiB;AAAA,YACvC,mBAAmB,OAAO,qBAAqB;AAAA,YAC/C,YAAY;AAAA,YACZ,WAAW;AAAA,YACX,eAAe,QAAQ,iBAAiB;AAAA,YACxC,gBAAgB,WAAW,IAAI,QAAM,EAAE,UAAU,EAAE,UAAU,aAAa,EAAE,YAAY,EAAE;AAAA,UAC5F,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,cAAY,OAAO,IAAI;AACzB;AAGO,SAAS,eAAe,QAAa,SAAwB;AAClE,QAAM,cAAc,OAAO;AAC3B,MAAI,YAAY,OAAO,EAAG;AAC1B,QAAM,WAAW,YAAY,OAAO,KAAK,WAAW;AAGpD,cAAY,SAAS,CAAC,SAAc,SAAoB;AACtD,UAAM,YAAY,OAAO;AACzB,UAAM,KAAK,YAAY,IAAI;AAE3B,UAAM,SAAS,SAAS,MAAM,GAAG,IAAI;AAGrC,QAAI,SAAS,IAAI,EAAG,QAAO;AAG3B,WAAO,QAAQ,QAAQ,CAAC,aAAkB;AACxC,YAAM,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,EAAE;AAEnD,mBAAa,MAAM;AACjB,YAAI;AACF,gBAAM,WAAW,MAAM,YAAY,CAAC;AACpC,gBAAM,SAAS,MAAM;AACrB,gBAAM,QAAQ,MAAM,SAAS;AAE7B,gBAAM,cAAc,SAChB,CAAC,EAAE,MAAM,UAAU,SAAS,OAAO,GAAG,GAAG,QAAQ,IACjD;AAEJ,gBAAM,UAAU,IAAI,aAAa;AACjC,gBAAM,CAAC,kBAAkB,gBAAgB,IAAI,cAAc,aAAa,OAAO;AAE/E,gBAAM,gBAAwB,UAAU,WAAW,CAAC,GAEjD,OAAO,CAAC,MAAW,EAAE,SAAS,MAAM,EAEpC,IAAI,CAAC,MAAW,EAAE,IAAc,EAChC,KAAK,EAAE;AAEV,gBAAM,iBAAiB,eAAe,MAAM,cAAc,OAAO,IAAI;AACrE,gBAAM,mBAAmB,gBAAgB,QAAQ;AACjD,gBAAM,aAAa,CAAC,GAAG,kBAAkB,GAAI,gBAAgB,cAAc,CAAC,CAAE;AAC9E,gBAAM,QAAQ,UAAU;AAExB,kBAAQ;AAAA,YACN,OAAO,UAAU,SAAS;AAAA,YAC1B,UAAU;AAAA,YACV,UAAU;AAAA,YACV,eAAe,OAAO,gBAAgB;AAAA,YACtC,mBAAmB,OAAO,iBAAiB;AAAA,YAC3C,YAAY;AAAA,YACZ,WAAW;AAAA,YACX,eAAe,UAAU,eAAe;AAAA,YACxC,gBAAgB,WAAW,IAAI,QAAM,EAAE,UAAU,EAAE,UAAU,aAAa,EAAE,YAAY,EAAE;AAAA,UAC5F,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,cAAY,OAAO,IAAI;AACzB;;;ACxLA,IAAM,QAIF;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AACT;AAEO,SAAS,KAAK,EAAE,QAAQ,UAAU,6BAA6B,QAAQ,MAAM,GAAsB;AACxG,MAAI;AACF,UAAM,SAAS;AACf,UAAM,UAAU,QAAQ,QAAQ,OAAO,EAAE;AAEzC,QAAI,OAAO;AACT,cAAQ,MAAM,+BAA+B,MAAM,OAAO;AAAA,IAC5D;AAEA,UAAM,QAAQ,IAAI,SAAS,aAAW,KAAK,QAAQ,SAAS,MAAM,OAAO,CAAC;AAAA,EAC5E,SAAS,KAAK;AAEZ,UAAM,QAAQ;AACd,SAAK,iCAAiC,GAAG;AAAA,EAC3C;AACF;AAIO,SAAS,WAAW,QAAmB;AAC5C,MAAI,CAAC,MAAM,OAAO;AAChB,SAAK,iFAA4E;AACjF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,UAAU,MAAM,MAAM,QAAQ,KAAK,MAAM,KAAK;AAEpD,QAAI,QAAQ,MAAM,aAAa;AAC7B,kBAAY,QAAQ,OAAO;AAC3B;AAAA,IACF;AAEA,QAAI,QAAQ,UAAU;AACpB,qBAAe,QAAQ,OAAO;AAC9B;AAAA,IACF;AAEA;AAAA,MACE;AAAA,IAEF;AAAA,EACF,SAAS,KAAK;AACZ,SAAK,qDAAqD,GAAG;AAAA,EAC/D;AACF;AAEA,SAAS,KAAK,SAAiB,KAAqB;AAClD,MAAI;AACF,YAAQ,KAAK,eAAe,OAAO,IAAI,GAAI,QAAQ,SAAY,CAAC,IAAI,CAAC,GAAG,CAAE;AAAA,EAC5E,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,WAAiB;AAC/B,QAAM,QAAQ;AAChB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["export { init, instrument, shutdown } from './client'\nexport type { InitOptions } from './client'\nexport type { Detection, ScrubResult } from './scrubber'\n","const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\nconst RETRYABLE_STATUS = new Set([408, 425, 429])\n\n/**\n * sent: accepted by the server\n * retry: temporary failure, keep the record and try again later\n * drop: permanently rejected, retrying the same record can never succeed\n */\nexport type SendOutcome = 'sent' | 'retry' | 'drop'\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<SendOutcome> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const controller = new AbortController()\n timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n timer.unref?.()\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (response.status < 400) return 'sent'\n if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return 'retry'\n\n // 400 (invalid record), 401/403 (bad or revoked key), 413 (too large)...\n // Retrying would block every later record behind this one.\n try {\n console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`)\n } catch {\n // never propagate\n }\n return 'drop'\n } catch {\n return 'retry'\n } finally {\n clearTimeout(timer)\n }\n}\n","import type { SendOutcome } from './sender'\n\ntype Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<SendOutcome | boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\n// unref(): a pending retry must never keep the host process alive. Records still\n// queued when the app exits are lost, as documented.\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(resolve, ms)\n timer.unref?.()\n })\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let done = false\n try {\n const outcome = await this.sendFn(this.items[0])\n // 'sent' and 'drop' both finish this record; true is accepted as 'sent'.\n done = outcome === true || outcome === 'sent' || outcome === 'drop'\n } catch {\n // never propagate\n }\n\n if (done) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","import { createHmac } from 'node:crypto'\n\nexport interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Hex chars of the HMAC digest shown in a token. 12 hex = 48 bits: enough to keep\n// collisions negligible at realistic volumes while staying readable.\nconst TOKEN_HEX_LEN = 12\n\n// Each rule: [category, regex (must have g flag), placeholder base].\n// Kept identical to the Python SDK, so both produce the same tokens for the same key.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n // IP before phone: the phone rule would otherwise swallow dotted quads\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n // IBAN: tolerate the usual 4-character grouping (\"DE89 3704 0044 ...\") so a spaced\n // IBAN can't leak its digits to the phone rule\n ['iban', /\\b[A-Z]{2}\\d{2}(?:[ ]?[A-Z0-9]){11,30}\\b/g, 'IBAN_REDACTED'],\n // Card numbers before phone, so the whole grouped number (13–20 digits) is caught.\n // Reported as id_number: the ingest API has no card category.\n ['id_number', /\\b\\d{4}(?:[ -]?\\d{4}){2,4}\\b/g, 'CARD_REDACTED'],\n // (?<![\\w+]) instead of \\b so a leading \"+\" is part of the match, not left behind\n ['phone', /(?<![\\w+])\\+?\\d[\\d\\s\\-().]{5,14}\\d\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n // Names last: a broad heuristic, applied only after a title\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\n/** Folds formatting differences (spaced IBAN, email case) so one value gets one placeholder. */\nfunction normalize(value: string): string {\n return value.replace(/[\\s-]/g, '').toLowerCase()\n}\n\n/**\n * Chooses the placeholder for each detected value in one record (all messages and\n * the response).\n *\n * With a `piiKey`, each value becomes a deterministic, irreversible HMAC token:\n * the same value gets the same token in every log, so Sovergate can count unique\n * people without ever holding the value. The key never leaves your infrastructure.\n *\n * Without a key, placeholders are numbered within the record: the same value\n * always gets the same number and two values never share one, but numbers can't\n * be matched across records.\n */\nexport class ScrubContext {\n private readonly seen = new Map<string, number>()\n private readonly next = new Map<string, number>()\n\n constructor(private readonly piiKey?: string) {}\n\n placeholder(base: string, value: string): string {\n if (this.piiKey) {\n const digest = createHmac('sha256', this.piiKey).update(normalize(value), 'utf8').digest('hex')\n return `[${base}_${digest.slice(0, TOKEN_HEX_LEN)}]`\n }\n\n const key = `${base}\\u0000${normalize(value)}`\n let n = this.seen.get(key)\n if (n === undefined) {\n n = (this.next.get(base) ?? 0) + 1\n this.next.set(base, n)\n this.seen.set(key, n)\n }\n return `[${base}_${n}]`\n }\n}\n\nexport function scrub(text: string, context: ScrubContext = new ScrubContext()): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, match => {\n const placeholder = context.placeholder(basePlaceholder, match)\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(\n messages: OpenAIMessage[],\n context: ScrubContext = new ScrubContext(),\n): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content, context)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text, context)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { ScrubContext, scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop, proxy) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n // The native .catch() and .finally() would call the target's own then(),\n // skipping the hook below; route them through the proxy's then().\n if (prop === 'catch' || prop === 'finally') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (Promise.prototype[prop] as (...a: unknown[]) => Promise<any>).apply(proxy, args)\n }\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\nfunction safely(fn: () => void): void {\n try {\n fn()\n } catch {\n // never let logging errors surface to the caller\n }\n}\n\n/** Rebuilds, from a stream's chunks, the parts of the full response that are logged. */\ninterface StreamCollector {\n add(chunk: unknown): void\n /** The response as a non-streamed call would have returned it, or null if nothing arrived. */\n response(): unknown\n}\n\n/**\n * Logs a provider stream once it ends, is abandoned part-way, or fails. Only the\n * stream object's own async iterator is replaced: the app gets the same object\n * (its class, `.controller`, `.tee()` and `.toReadableStream()` keep working) and\n * every chunk unchanged as it arrives. A copy of the text is collected on the side.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction instrumentStream(stream: any, collector: StreamCollector, done: (response: unknown) => void): void {\n if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') return\n const iterate = stream[Symbol.asyncIterator]\n\n let finished = false\n const finish = (): void => {\n if (finished) return\n finished = true\n safely(() => {\n const response = collector.response()\n if (response) done(response)\n })\n }\n\n stream[Symbol.asyncIterator] = function () {\n const iterator = iterate.call(this)\n return {\n async next(...args: unknown[]) {\n let result\n try {\n result = await iterator.next(...args)\n } catch (err) {\n finish()\n throw err\n }\n if (result.done) finish()\n else safely(() => collector.add(result.value))\n return result\n },\n // `break` out of a for await loop lands here.\n async return(value?: unknown) {\n finish()\n return typeof iterator.return === 'function' ? iterator.return(value) : { done: true, value }\n },\n async throw(err?: unknown) {\n finish()\n if (typeof iterator.throw === 'function') return iterator.throw(err)\n throw err\n },\n [Symbol.asyncIterator]() {\n return this\n },\n }\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction openAIStreamCollector(): StreamCollector {\n let seen = false\n let model: string | undefined\n let finishReason: string | null = null\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let usage: any\n const parts: string[] = []\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n add(chunk: any) {\n seen = true\n model = chunk?.model ?? model\n // Only sent when the app asks for it with stream_options: { include_usage: true }.\n usage = chunk?.usage ?? usage\n for (const choice of chunk?.choices ?? []) {\n if ((choice?.index ?? 0) !== 0) continue\n if (choice?.delta?.content) parts.push(choice.delta.content)\n finishReason = choice?.finish_reason ?? finishReason\n }\n },\n response() {\n if (!seen) return null\n return { model, choices: [{ message: { content: parts.join('') }, finish_reason: finishReason }], usage }\n },\n }\n}\n\nfunction anthropicStreamCollector(): StreamCollector {\n let seen = false\n let model: string | undefined\n let stopReason: string | null = null\n let inputTokens = 0\n let outputTokens = 0\n const parts: string[] = []\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n add(event: any) {\n seen = true\n if (event?.type === 'message_start') {\n model = event.message?.model\n inputTokens = event.message?.usage?.input_tokens ?? 0\n outputTokens = event.message?.usage?.output_tokens ?? 0\n } else if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {\n parts.push(event.delta.text)\n } else if (event?.type === 'message_delta') {\n stopReason = event.delta?.stop_reason ?? stopReason\n outputTokens = event.usage?.output_tokens ?? outputTokens\n inputTokens = event.usage?.input_tokens ?? inputTokens\n }\n },\n response() {\n if (!seen) return null\n return {\n model,\n content: [{ type: 'text', text: parts.join('') }],\n stop_reason: stopReason,\n usage: { input_tokens: inputTokens, output_tokens: outputTokens },\n }\n },\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BuildPayload = (opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) => Record<string, unknown>\n\n/**\n * Wraps `target.create`. Plain calls are logged when the app reads the result;\n * streamed calls when the stream ends, so their latency covers the whole response.\n * The provider SDKs' own `.stream()` helpers call `create({ stream: true })`, so\n * they are covered too.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction wrapCreate(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n target: any,\n enqueue: Enqueue,\n build: BuildPayload,\n collector: () => StreamCollector,\n piiKey?: string,\n): void {\n if (target[PATCHED]) return\n const original = target.create.bind(target)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n target.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n const done = (response: unknown): void => {\n const latencyMs = Math.round(performance.now() - t0)\n setImmediate(() => safely(() => enqueue(build(opts, response, requestAt, latencyMs, piiKey))))\n }\n\n if (isStream(opts)) return observe(result, (stream: unknown) => instrumentStream(stream, collector(), done))\n return observe(result, done)\n }\n target[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction buildOpenAIPayload(opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const context = new ScrubContext(piiKey)\n const [scrubbedMessages, promptDetections] = scrubMessages(messages, context)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n return {\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction buildAnthropicPayload(opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const context = new ScrubContext(piiKey)\n const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n return {\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue, piiKey?: string): void {\n wrapCreate(client.chat.completions, enqueue, buildOpenAIPayload, openAIStreamCollector, piiKey)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue, piiKey?: string): void {\n wrapCreate(client.messages, enqueue, buildAnthropicPayload, anthropicStreamCollector, piiKey)\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n /**\n * Secret used to turn detected personal data into stable, irreversible tokens.\n * The same value always maps to the same token, so Sovergate can count unique\n * people without holding the data. Keep it secret and stable: it never leaves\n * your infrastructure. Without it, placeholders are numbered per record.\n */\n piiKey?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n piiKey: string | undefined\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n piiKey: undefined,\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', piiKey, debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n state.piiKey = piiKey || undefined\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n if (!state.piiKey) {\n console.debug('[sovergate] no piiKey set: PII placeholders are numbered per record and not correlatable across logs')\n }\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue, state.piiKey)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue, state.piiKey)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n state.piiKey = undefined\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -3,9 +3,11 @@ var BASE_URL = "https://api.sovergate.com";
3
3
  var TIMEOUT_MS = 2e3;
4
4
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
5
5
  async function send(apiKey, payload, baseUrl = BASE_URL) {
6
+ let timer;
6
7
  try {
7
8
  const controller = new AbortController();
8
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
9
+ timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
10
+ timer.unref?.();
9
11
  const response = await fetch(`${baseUrl}/ingest`, {
10
12
  method: "POST",
11
13
  headers: {
@@ -15,7 +17,6 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
15
17
  body: JSON.stringify(payload),
16
18
  signal: controller.signal
17
19
  });
18
- clearTimeout(timer);
19
20
  if (response.status < 400) return "sent";
20
21
  if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return "retry";
21
22
  try {
@@ -25,6 +26,8 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
25
26
  return "drop";
26
27
  } catch {
27
28
  return "retry";
29
+ } finally {
30
+ clearTimeout(timer);
28
31
  }
29
32
  }
30
33
 
@@ -32,7 +35,10 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
32
35
  var MAX_QUEUE = 1e4;
33
36
  var RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60];
34
37
  function sleep(ms) {
35
- return new Promise((resolve) => setTimeout(resolve, ms));
38
+ return new Promise((resolve) => {
39
+ const timer = setTimeout(resolve, ms);
40
+ timer.unref?.();
41
+ });
36
42
  }
37
43
  var LogQueue = class {
38
44
  sendFn;
@@ -72,21 +78,41 @@ var LogQueue = class {
72
78
  };
73
79
 
74
80
  // src/scrubber.ts
81
+ import { createHmac } from "crypto";
82
+ var TOKEN_HEX_LEN = 12;
75
83
  var RULES = [
76
84
  ["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
85
+ // IP before phone: the phone rule would otherwise swallow dotted quads
77
86
  ["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
78
- ["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
87
+ // IBAN: tolerate the usual 4-character grouping ("DE89 3704 0044 ...") so a spaced
88
+ // IBAN can't leak its digits to the phone rule
89
+ ["iban", /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}\b/g, "IBAN_REDACTED"],
90
+ // Card numbers before phone, so the whole grouped number (13–20 digits) is caught.
91
+ // Reported as id_number: the ingest API has no card category.
92
+ ["id_number", /\b\d{4}(?:[ -]?\d{4}){2,4}\b/g, "CARD_REDACTED"],
79
93
  // (?<![\w+]) instead of \b so a leading "+" is part of the match, not left behind
80
- ["phone", /(?<![\w+])\+?\d[\d\s\-(). ]{5,14}\d\b/g, "PHONE_REDACTED"],
94
+ ["phone", /(?<![\w+])\+?\d[\d\s\-().]{5,14}\d\b/g, "PHONE_REDACTED"],
81
95
  ["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
82
96
  ["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
97
+ // Names last: a broad heuristic, applied only after a title
83
98
  ["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
84
99
  ];
100
+ function normalize(value) {
101
+ return value.replace(/[\s-]/g, "").toLowerCase();
102
+ }
85
103
  var ScrubContext = class {
104
+ constructor(piiKey) {
105
+ this.piiKey = piiKey;
106
+ }
107
+ piiKey;
86
108
  seen = /* @__PURE__ */ new Map();
87
109
  next = /* @__PURE__ */ new Map();
88
110
  placeholder(base, value) {
89
- const key = `${base}\0${value.replace(/[\s-]/g, "").toLowerCase()}`;
111
+ if (this.piiKey) {
112
+ const digest = createHmac("sha256", this.piiKey).update(normalize(value), "utf8").digest("hex");
113
+ return `[${base}_${digest.slice(0, TOKEN_HEX_LEN)}]`;
114
+ }
115
+ const key = `${base}\0${normalize(value)}`;
90
116
  let n = this.seen.get(key);
91
117
  if (n === void 0) {
92
118
  n = (this.next.get(base) ?? 0) + 1;
@@ -154,9 +180,15 @@ function observe(result, onData) {
154
180
  };
155
181
  try {
156
182
  return new Proxy(result, {
157
- get(target, prop) {
183
+ get(target, prop, proxy) {
158
184
  const value = Reflect.get(target, prop, target);
159
185
  if (typeof value !== "function") return value;
186
+ if (prop === "catch" || prop === "finally") {
187
+ return (...args) => (
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ Promise.prototype[prop].apply(proxy, args)
190
+ );
191
+ }
160
192
  if (prop === "then") {
161
193
  return (onFulfilled, onRejected) => value.call(
162
194
  target,
@@ -187,102 +219,200 @@ function observe(result, onData) {
187
219
  function isStream(opts) {
188
220
  return opts?.stream === true;
189
221
  }
190
- function patchOpenAI(client, enqueue) {
191
- const completions = client.chat.completions;
192
- if (completions[PATCHED]) return;
193
- const original = completions.create.bind(completions);
194
- completions.create = (opts, ...rest) => {
195
- const requestAt = utcnow();
196
- const t0 = performance.now();
197
- const result = original(opts, ...rest);
198
- if (isStream(opts)) return result;
199
- return observe(result, (response) => {
200
- const latencyMs = Math.round(performance.now() - t0);
201
- setImmediate(() => {
222
+ function safely(fn) {
223
+ try {
224
+ fn();
225
+ } catch {
226
+ }
227
+ }
228
+ function instrumentStream(stream, collector, done) {
229
+ if (!stream || typeof stream[Symbol.asyncIterator] !== "function") return;
230
+ const iterate = stream[Symbol.asyncIterator];
231
+ let finished = false;
232
+ const finish = () => {
233
+ if (finished) return;
234
+ finished = true;
235
+ safely(() => {
236
+ const response = collector.response();
237
+ if (response) done(response);
238
+ });
239
+ };
240
+ stream[Symbol.asyncIterator] = function() {
241
+ const iterator = iterate.call(this);
242
+ return {
243
+ async next(...args) {
244
+ let result;
202
245
  try {
203
- const messages = opts?.messages ?? [];
204
- const model = opts?.model ?? "";
205
- const context = new ScrubContext();
206
- const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
207
- const choice = response?.choices?.[0];
208
- const responseText = choice?.message?.content ?? "";
209
- const scrubbedResult = responseText ? scrub(responseText, context) : null;
210
- const scrubbedResponse = scrubbedResult?.text ?? "";
211
- const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
212
- const usage = response?.usage;
213
- enqueue({
214
- model: response?.model ?? model,
215
- messages: scrubbedMessages,
216
- response: scrubbedResponse,
217
- prompt_tokens: usage?.prompt_tokens ?? 0,
218
- completion_tokens: usage?.completion_tokens ?? 0,
219
- latency_ms: latencyMs,
220
- timestamp: requestAt,
221
- finish_reason: choice?.finish_reason ?? null,
222
- pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
223
- });
224
- } catch {
246
+ result = await iterator.next(...args);
247
+ } catch (err) {
248
+ finish();
249
+ throw err;
225
250
  }
226
- });
227
- });
251
+ if (result.done) finish();
252
+ else safely(() => collector.add(result.value));
253
+ return result;
254
+ },
255
+ // `break` out of a for await loop lands here.
256
+ async return(value) {
257
+ finish();
258
+ return typeof iterator.return === "function" ? iterator.return(value) : { done: true, value };
259
+ },
260
+ async throw(err) {
261
+ finish();
262
+ if (typeof iterator.throw === "function") return iterator.throw(err);
263
+ throw err;
264
+ },
265
+ [Symbol.asyncIterator]() {
266
+ return this;
267
+ }
268
+ };
269
+ };
270
+ }
271
+ function openAIStreamCollector() {
272
+ let seen = false;
273
+ let model;
274
+ let finishReason = null;
275
+ let usage;
276
+ const parts = [];
277
+ return {
278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
+ add(chunk) {
280
+ seen = true;
281
+ model = chunk?.model ?? model;
282
+ usage = chunk?.usage ?? usage;
283
+ for (const choice of chunk?.choices ?? []) {
284
+ if ((choice?.index ?? 0) !== 0) continue;
285
+ if (choice?.delta?.content) parts.push(choice.delta.content);
286
+ finishReason = choice?.finish_reason ?? finishReason;
287
+ }
288
+ },
289
+ response() {
290
+ if (!seen) return null;
291
+ return { model, choices: [{ message: { content: parts.join("") }, finish_reason: finishReason }], usage };
292
+ }
293
+ };
294
+ }
295
+ function anthropicStreamCollector() {
296
+ let seen = false;
297
+ let model;
298
+ let stopReason = null;
299
+ let inputTokens = 0;
300
+ let outputTokens = 0;
301
+ const parts = [];
302
+ return {
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ add(event) {
305
+ seen = true;
306
+ if (event?.type === "message_start") {
307
+ model = event.message?.model;
308
+ inputTokens = event.message?.usage?.input_tokens ?? 0;
309
+ outputTokens = event.message?.usage?.output_tokens ?? 0;
310
+ } else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
311
+ parts.push(event.delta.text);
312
+ } else if (event?.type === "message_delta") {
313
+ stopReason = event.delta?.stop_reason ?? stopReason;
314
+ outputTokens = event.usage?.output_tokens ?? outputTokens;
315
+ inputTokens = event.usage?.input_tokens ?? inputTokens;
316
+ }
317
+ },
318
+ response() {
319
+ if (!seen) return null;
320
+ return {
321
+ model,
322
+ content: [{ type: "text", text: parts.join("") }],
323
+ stop_reason: stopReason,
324
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens }
325
+ };
326
+ }
228
327
  };
229
- completions[PATCHED] = true;
230
328
  }
231
- function patchAnthropic(client, enqueue) {
232
- const messagesApi = client.messages;
233
- if (messagesApi[PATCHED]) return;
234
- const original = messagesApi.create.bind(messagesApi);
235
- messagesApi.create = (opts, ...rest) => {
329
+ function wrapCreate(target, enqueue, build, collector, piiKey) {
330
+ if (target[PATCHED]) return;
331
+ const original = target.create.bind(target);
332
+ target.create = (opts, ...rest) => {
236
333
  const requestAt = utcnow();
237
334
  const t0 = performance.now();
238
335
  const result = original(opts, ...rest);
239
- if (isStream(opts)) return result;
240
- return observe(result, (response) => {
336
+ const done = (response) => {
241
337
  const latencyMs = Math.round(performance.now() - t0);
242
- setImmediate(() => {
243
- try {
244
- const messages = opts?.messages ?? [];
245
- const system = opts?.system;
246
- const model = opts?.model ?? "";
247
- const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
248
- const context = new ScrubContext();
249
- const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
250
- const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
251
- const scrubbedResult = responseText ? scrub(responseText, context) : null;
252
- const scrubbedResponse = scrubbedResult?.text ?? "";
253
- const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
254
- const usage = response?.usage;
255
- enqueue({
256
- model: response?.model ?? model,
257
- messages: scrubbedMessages,
258
- response: scrubbedResponse,
259
- prompt_tokens: usage?.input_tokens ?? 0,
260
- completion_tokens: usage?.output_tokens ?? 0,
261
- latency_ms: latencyMs,
262
- timestamp: requestAt,
263
- finish_reason: response?.stop_reason ?? null,
264
- pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
265
- });
266
- } catch {
267
- }
268
- });
269
- });
338
+ setImmediate(() => safely(() => enqueue(build(opts, response, requestAt, latencyMs, piiKey))));
339
+ };
340
+ if (isStream(opts)) return observe(result, (stream) => instrumentStream(stream, collector(), done));
341
+ return observe(result, done);
342
+ };
343
+ target[PATCHED] = true;
344
+ }
345
+ function buildOpenAIPayload(opts, response, requestAt, latencyMs, piiKey) {
346
+ const messages = opts?.messages ?? [];
347
+ const model = opts?.model ?? "";
348
+ const context = new ScrubContext(piiKey);
349
+ const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
350
+ const choice = response?.choices?.[0];
351
+ const responseText = choice?.message?.content ?? "";
352
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
353
+ const scrubbedResponse = scrubbedResult?.text ?? "";
354
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
355
+ const usage = response?.usage;
356
+ return {
357
+ model: response?.model ?? model,
358
+ messages: scrubbedMessages,
359
+ response: scrubbedResponse,
360
+ prompt_tokens: usage?.prompt_tokens ?? 0,
361
+ completion_tokens: usage?.completion_tokens ?? 0,
362
+ latency_ms: latencyMs,
363
+ timestamp: requestAt,
364
+ finish_reason: choice?.finish_reason ?? null,
365
+ pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
270
366
  };
271
- messagesApi[PATCHED] = true;
367
+ }
368
+ function buildAnthropicPayload(opts, response, requestAt, latencyMs, piiKey) {
369
+ const messages = opts?.messages ?? [];
370
+ const system = opts?.system;
371
+ const model = opts?.model ?? "";
372
+ const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
373
+ const context = new ScrubContext(piiKey);
374
+ const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
375
+ const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
376
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
377
+ const scrubbedResponse = scrubbedResult?.text ?? "";
378
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
379
+ const usage = response?.usage;
380
+ return {
381
+ model: response?.model ?? model,
382
+ messages: scrubbedMessages,
383
+ response: scrubbedResponse,
384
+ prompt_tokens: usage?.input_tokens ?? 0,
385
+ completion_tokens: usage?.output_tokens ?? 0,
386
+ latency_ms: latencyMs,
387
+ timestamp: requestAt,
388
+ finish_reason: response?.stop_reason ?? null,
389
+ pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
390
+ };
391
+ }
392
+ function patchOpenAI(client, enqueue, piiKey) {
393
+ wrapCreate(client.chat.completions, enqueue, buildOpenAIPayload, openAIStreamCollector, piiKey);
394
+ }
395
+ function patchAnthropic(client, enqueue, piiKey) {
396
+ wrapCreate(client.messages, enqueue, buildAnthropicPayload, anthropicStreamCollector, piiKey);
272
397
  }
273
398
 
274
399
  // src/client.ts
275
400
  var state = {
276
401
  apiKey: null,
277
402
  baseUrl: "https://api.sovergate.com",
403
+ piiKey: void 0,
278
404
  queue: null
279
405
  };
280
- function init({ apiKey, baseUrl = "https://api.sovergate.com", debug = false }) {
406
+ function init({ apiKey, baseUrl = "https://api.sovergate.com", piiKey, debug = false }) {
281
407
  try {
282
408
  state.apiKey = apiKey;
283
409
  state.baseUrl = baseUrl.replace(/\/$/, "");
410
+ state.piiKey = piiKey || void 0;
284
411
  if (debug) {
285
412
  console.debug("[sovergate] init baseUrl=%s", state.baseUrl);
413
+ if (!state.piiKey) {
414
+ console.debug("[sovergate] no piiKey set: PII placeholders are numbered per record and not correlatable across logs");
415
+ }
286
416
  }
287
417
  state.queue = new LogQueue((payload) => send(apiKey, payload, state.baseUrl));
288
418
  } catch (err) {
@@ -298,11 +428,11 @@ function instrument(client) {
298
428
  try {
299
429
  const enqueue = state.queue.enqueue.bind(state.queue);
300
430
  if (client?.chat?.completions) {
301
- patchOpenAI(client, enqueue);
431
+ patchOpenAI(client, enqueue, state.piiKey);
302
432
  return;
303
433
  }
304
434
  if (client?.messages) {
305
- patchAnthropic(client, enqueue);
435
+ patchAnthropic(client, enqueue, state.piiKey);
306
436
  return;
307
437
  }
308
438
  warn(
@@ -320,6 +450,7 @@ function warn(message, err) {
320
450
  }
321
451
  function shutdown() {
322
452
  state.queue = null;
453
+ state.piiKey = void 0;
323
454
  }
324
455
  export {
325
456
  init,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\nconst RETRYABLE_STATUS = new Set([408, 425, 429])\n\n/**\n * sent: accepted by the server\n * retry: temporary failure, keep the record and try again later\n * drop: permanently rejected, retrying the same record can never succeed\n */\nexport type SendOutcome = 'sent' | 'retry' | 'drop'\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<SendOutcome> {\n try {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n if (response.status < 400) return 'sent'\n if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return 'retry'\n\n // 400 (invalid record), 401/403 (bad or revoked key), 413 (too large)...\n // Retrying would block every later record behind this one.\n try {\n console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`)\n } catch {\n // never propagate\n }\n return 'drop'\n } catch {\n return 'retry'\n }\n}\n","import type { SendOutcome } from './sender'\n\ntype Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<SendOutcome | boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let done = false\n try {\n const outcome = await this.sendFn(this.items[0])\n // 'sent' and 'drop' both finish this record; true is accepted as 'sent'.\n done = outcome === true || outcome === 'sent' || outcome === 'drop'\n } catch {\n // never propagate\n }\n\n if (done) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","export interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Each rule: [category, regex (must have g flag), placeholder base]\n// Order matters — IP before phone to avoid dotted-quad false matches.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n ['iban', /\\b[A-Z]{2}\\d{2}[A-Z0-9]{4,30}\\b/g, 'IBAN_REDACTED'],\n // (?<![\\w+]) instead of \\b so a leading \"+\" is part of the match, not left behind\n ['phone', /(?<![\\w+])\\+?\\d[\\d\\s\\-(). ]{5,14}\\d\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\n/**\n * Numbers placeholders across one whole record (all messages and the response),\n * so the same value always gets the same placeholder and two different values\n * never share one.\n */\nexport class ScrubContext {\n private readonly seen = new Map<string, number>()\n private readonly next = new Map<string, number>()\n\n placeholder(base: string, value: string): string {\n const key = `${base}\\u0000${value.replace(/[\\s-]/g, '').toLowerCase()}`\n let n = this.seen.get(key)\n if (n === undefined) {\n n = (this.next.get(base) ?? 0) + 1\n this.next.set(base, n)\n this.seen.set(key, n)\n }\n return `[${base}_${n}]`\n }\n}\n\nexport function scrub(text: string, context: ScrubContext = new ScrubContext()): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, match => {\n const placeholder = context.placeholder(basePlaceholder, match)\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(\n messages: OpenAIMessage[],\n context: ScrubContext = new ScrubContext(),\n): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content, context)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text, context)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { ScrubContext, scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue): void {\n const completions = client.chat.completions\n if (completions[PATCHED]) return\n const original = completions.create.bind(completions)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n completions.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const context = new ScrubContext()\n const [scrubbedMessages, promptDetections] = scrubMessages(messages, context)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n completions[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue): void {\n const messagesApi = client.messages\n if (messagesApi[PATCHED]) return\n const original = messagesApi.create.bind(messagesApi)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messagesApi.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const context = new ScrubContext()\n const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n messagesApi[PATCHED] = true\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\nconst RETRYABLE_STATUS = new Set([408, 425, 429])\n\n/**\n * sent: accepted by the server\n * retry: temporary failure, keep the record and try again later\n * drop: permanently rejected, retrying the same record can never succeed\n */\nexport type SendOutcome = 'sent' | 'retry' | 'drop'\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<SendOutcome> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const controller = new AbortController()\n timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n timer.unref?.()\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (response.status < 400) return 'sent'\n if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return 'retry'\n\n // 400 (invalid record), 401/403 (bad or revoked key), 413 (too large)...\n // Retrying would block every later record behind this one.\n try {\n console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`)\n } catch {\n // never propagate\n }\n return 'drop'\n } catch {\n return 'retry'\n } finally {\n clearTimeout(timer)\n }\n}\n","import type { SendOutcome } from './sender'\n\ntype Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<SendOutcome | boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\n// unref(): a pending retry must never keep the host process alive. Records still\n// queued when the app exits are lost, as documented.\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(resolve, ms)\n timer.unref?.()\n })\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let done = false\n try {\n const outcome = await this.sendFn(this.items[0])\n // 'sent' and 'drop' both finish this record; true is accepted as 'sent'.\n done = outcome === true || outcome === 'sent' || outcome === 'drop'\n } catch {\n // never propagate\n }\n\n if (done) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","import { createHmac } from 'node:crypto'\n\nexport interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Hex chars of the HMAC digest shown in a token. 12 hex = 48 bits: enough to keep\n// collisions negligible at realistic volumes while staying readable.\nconst TOKEN_HEX_LEN = 12\n\n// Each rule: [category, regex (must have g flag), placeholder base].\n// Kept identical to the Python SDK, so both produce the same tokens for the same key.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n // IP before phone: the phone rule would otherwise swallow dotted quads\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n // IBAN: tolerate the usual 4-character grouping (\"DE89 3704 0044 ...\") so a spaced\n // IBAN can't leak its digits to the phone rule\n ['iban', /\\b[A-Z]{2}\\d{2}(?:[ ]?[A-Z0-9]){11,30}\\b/g, 'IBAN_REDACTED'],\n // Card numbers before phone, so the whole grouped number (13–20 digits) is caught.\n // Reported as id_number: the ingest API has no card category.\n ['id_number', /\\b\\d{4}(?:[ -]?\\d{4}){2,4}\\b/g, 'CARD_REDACTED'],\n // (?<![\\w+]) instead of \\b so a leading \"+\" is part of the match, not left behind\n ['phone', /(?<![\\w+])\\+?\\d[\\d\\s\\-().]{5,14}\\d\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n // Names last: a broad heuristic, applied only after a title\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\n/** Folds formatting differences (spaced IBAN, email case) so one value gets one placeholder. */\nfunction normalize(value: string): string {\n return value.replace(/[\\s-]/g, '').toLowerCase()\n}\n\n/**\n * Chooses the placeholder for each detected value in one record (all messages and\n * the response).\n *\n * With a `piiKey`, each value becomes a deterministic, irreversible HMAC token:\n * the same value gets the same token in every log, so Sovergate can count unique\n * people without ever holding the value. The key never leaves your infrastructure.\n *\n * Without a key, placeholders are numbered within the record: the same value\n * always gets the same number and two values never share one, but numbers can't\n * be matched across records.\n */\nexport class ScrubContext {\n private readonly seen = new Map<string, number>()\n private readonly next = new Map<string, number>()\n\n constructor(private readonly piiKey?: string) {}\n\n placeholder(base: string, value: string): string {\n if (this.piiKey) {\n const digest = createHmac('sha256', this.piiKey).update(normalize(value), 'utf8').digest('hex')\n return `[${base}_${digest.slice(0, TOKEN_HEX_LEN)}]`\n }\n\n const key = `${base}\\u0000${normalize(value)}`\n let n = this.seen.get(key)\n if (n === undefined) {\n n = (this.next.get(base) ?? 0) + 1\n this.next.set(base, n)\n this.seen.set(key, n)\n }\n return `[${base}_${n}]`\n }\n}\n\nexport function scrub(text: string, context: ScrubContext = new ScrubContext()): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, match => {\n const placeholder = context.placeholder(basePlaceholder, match)\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(\n messages: OpenAIMessage[],\n context: ScrubContext = new ScrubContext(),\n): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content, context)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text, context)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { ScrubContext, scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop, proxy) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n // The native .catch() and .finally() would call the target's own then(),\n // skipping the hook below; route them through the proxy's then().\n if (prop === 'catch' || prop === 'finally') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (Promise.prototype[prop] as (...a: unknown[]) => Promise<any>).apply(proxy, args)\n }\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\nfunction safely(fn: () => void): void {\n try {\n fn()\n } catch {\n // never let logging errors surface to the caller\n }\n}\n\n/** Rebuilds, from a stream's chunks, the parts of the full response that are logged. */\ninterface StreamCollector {\n add(chunk: unknown): void\n /** The response as a non-streamed call would have returned it, or null if nothing arrived. */\n response(): unknown\n}\n\n/**\n * Logs a provider stream once it ends, is abandoned part-way, or fails. Only the\n * stream object's own async iterator is replaced: the app gets the same object\n * (its class, `.controller`, `.tee()` and `.toReadableStream()` keep working) and\n * every chunk unchanged as it arrives. A copy of the text is collected on the side.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction instrumentStream(stream: any, collector: StreamCollector, done: (response: unknown) => void): void {\n if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') return\n const iterate = stream[Symbol.asyncIterator]\n\n let finished = false\n const finish = (): void => {\n if (finished) return\n finished = true\n safely(() => {\n const response = collector.response()\n if (response) done(response)\n })\n }\n\n stream[Symbol.asyncIterator] = function () {\n const iterator = iterate.call(this)\n return {\n async next(...args: unknown[]) {\n let result\n try {\n result = await iterator.next(...args)\n } catch (err) {\n finish()\n throw err\n }\n if (result.done) finish()\n else safely(() => collector.add(result.value))\n return result\n },\n // `break` out of a for await loop lands here.\n async return(value?: unknown) {\n finish()\n return typeof iterator.return === 'function' ? iterator.return(value) : { done: true, value }\n },\n async throw(err?: unknown) {\n finish()\n if (typeof iterator.throw === 'function') return iterator.throw(err)\n throw err\n },\n [Symbol.asyncIterator]() {\n return this\n },\n }\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction openAIStreamCollector(): StreamCollector {\n let seen = false\n let model: string | undefined\n let finishReason: string | null = null\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let usage: any\n const parts: string[] = []\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n add(chunk: any) {\n seen = true\n model = chunk?.model ?? model\n // Only sent when the app asks for it with stream_options: { include_usage: true }.\n usage = chunk?.usage ?? usage\n for (const choice of chunk?.choices ?? []) {\n if ((choice?.index ?? 0) !== 0) continue\n if (choice?.delta?.content) parts.push(choice.delta.content)\n finishReason = choice?.finish_reason ?? finishReason\n }\n },\n response() {\n if (!seen) return null\n return { model, choices: [{ message: { content: parts.join('') }, finish_reason: finishReason }], usage }\n },\n }\n}\n\nfunction anthropicStreamCollector(): StreamCollector {\n let seen = false\n let model: string | undefined\n let stopReason: string | null = null\n let inputTokens = 0\n let outputTokens = 0\n const parts: string[] = []\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n add(event: any) {\n seen = true\n if (event?.type === 'message_start') {\n model = event.message?.model\n inputTokens = event.message?.usage?.input_tokens ?? 0\n outputTokens = event.message?.usage?.output_tokens ?? 0\n } else if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {\n parts.push(event.delta.text)\n } else if (event?.type === 'message_delta') {\n stopReason = event.delta?.stop_reason ?? stopReason\n outputTokens = event.usage?.output_tokens ?? outputTokens\n inputTokens = event.usage?.input_tokens ?? inputTokens\n }\n },\n response() {\n if (!seen) return null\n return {\n model,\n content: [{ type: 'text', text: parts.join('') }],\n stop_reason: stopReason,\n usage: { input_tokens: inputTokens, output_tokens: outputTokens },\n }\n },\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype BuildPayload = (opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) => Record<string, unknown>\n\n/**\n * Wraps `target.create`. Plain calls are logged when the app reads the result;\n * streamed calls when the stream ends, so their latency covers the whole response.\n * The provider SDKs' own `.stream()` helpers call `create({ stream: true })`, so\n * they are covered too.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction wrapCreate(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n target: any,\n enqueue: Enqueue,\n build: BuildPayload,\n collector: () => StreamCollector,\n piiKey?: string,\n): void {\n if (target[PATCHED]) return\n const original = target.create.bind(target)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n target.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n const done = (response: unknown): void => {\n const latencyMs = Math.round(performance.now() - t0)\n setImmediate(() => safely(() => enqueue(build(opts, response, requestAt, latencyMs, piiKey))))\n }\n\n if (isStream(opts)) return observe(result, (stream: unknown) => instrumentStream(stream, collector(), done))\n return observe(result, done)\n }\n target[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction buildOpenAIPayload(opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const context = new ScrubContext(piiKey)\n const [scrubbedMessages, promptDetections] = scrubMessages(messages, context)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n return {\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction buildAnthropicPayload(opts: any, response: any, requestAt: string, latencyMs: number, piiKey?: string) {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const context = new ScrubContext(piiKey)\n const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResult = responseText ? scrub(responseText, context) : null\n const scrubbedResponse = scrubbedResult?.text ?? ''\n const detections = [...promptDetections, ...(scrubbedResult?.detections ?? [])]\n const usage = response?.usage\n\n return {\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue, piiKey?: string): void {\n wrapCreate(client.chat.completions, enqueue, buildOpenAIPayload, openAIStreamCollector, piiKey)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue, piiKey?: string): void {\n wrapCreate(client.messages, enqueue, buildAnthropicPayload, anthropicStreamCollector, piiKey)\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n /**\n * Secret used to turn detected personal data into stable, irreversible tokens.\n * The same value always maps to the same token, so Sovergate can count unique\n * people without holding the data. Keep it secret and stable: it never leaves\n * your infrastructure. Without it, placeholders are numbered per record.\n */\n piiKey?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n piiKey: string | undefined\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n piiKey: undefined,\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', piiKey, debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n state.piiKey = piiKey || undefined\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n if (!state.piiKey) {\n console.debug('[sovergate] no piiKey set: PII placeholders are numbered per record and not correlatable across logs')\n }\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue, state.piiKey)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue, state.piiKey)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n state.piiKey = undefined\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sovergate",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "EU AI Act compliance SDK — automatic, non-invasive audit logging for LLM calls",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",