sovergate 0.1.0-alpha.1 → 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/LICENSE +21 -0
- package/README.md +118 -16
- package/dist/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +247 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +247 -89
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -2
package/dist/index.js
CHANGED
|
@@ -29,10 +29,13 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
// src/sender.ts
|
|
30
30
|
var BASE_URL = "https://api.sovergate.com";
|
|
31
31
|
var TIMEOUT_MS = 2e3;
|
|
32
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
32
33
|
async function send(apiKey, payload, baseUrl = BASE_URL) {
|
|
34
|
+
let timer;
|
|
33
35
|
try {
|
|
34
36
|
const controller = new AbortController();
|
|
35
|
-
|
|
37
|
+
timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
38
|
+
timer.unref?.();
|
|
36
39
|
const response = await fetch(`${baseUrl}/ingest`, {
|
|
37
40
|
method: "POST",
|
|
38
41
|
headers: {
|
|
@@ -42,10 +45,17 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
|
|
|
42
45
|
body: JSON.stringify(payload),
|
|
43
46
|
signal: controller.signal
|
|
44
47
|
});
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
if (response.status < 400) return "sent";
|
|
49
|
+
if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return "retry";
|
|
50
|
+
try {
|
|
51
|
+
console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
return "drop";
|
|
47
55
|
} catch {
|
|
48
|
-
return
|
|
56
|
+
return "retry";
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -53,7 +63,10 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
|
|
|
53
63
|
var MAX_QUEUE = 1e4;
|
|
54
64
|
var RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60];
|
|
55
65
|
function sleep(ms) {
|
|
56
|
-
return new Promise((resolve) =>
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(resolve, ms);
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
});
|
|
57
70
|
}
|
|
58
71
|
var LogQueue = class {
|
|
59
72
|
sendFn;
|
|
@@ -73,12 +86,13 @@ var LogQueue = class {
|
|
|
73
86
|
this.draining = true;
|
|
74
87
|
let retryIndex = 0;
|
|
75
88
|
while (this.items.length > 0) {
|
|
76
|
-
let
|
|
89
|
+
let done = false;
|
|
77
90
|
try {
|
|
78
|
-
|
|
91
|
+
const outcome = await this.sendFn(this.items[0]);
|
|
92
|
+
done = outcome === true || outcome === "sent" || outcome === "drop";
|
|
79
93
|
} catch {
|
|
80
94
|
}
|
|
81
|
-
if (
|
|
95
|
+
if (done) {
|
|
82
96
|
this.items.shift();
|
|
83
97
|
retryIndex = 0;
|
|
84
98
|
} else {
|
|
@@ -92,44 +106,77 @@ var LogQueue = class {
|
|
|
92
106
|
};
|
|
93
107
|
|
|
94
108
|
// src/scrubber.ts
|
|
109
|
+
var import_node_crypto = require("crypto");
|
|
110
|
+
var TOKEN_HEX_LEN = 12;
|
|
95
111
|
var RULES = [
|
|
96
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
|
|
97
114
|
["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
|
|
98
|
-
|
|
99
|
-
|
|
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"],
|
|
121
|
+
// (?<![\w+]) instead of \b so a leading "+" is part of the match, not left behind
|
|
122
|
+
["phone", /(?<![\w+])\+?\d[\d\s\-().]{5,14}\d\b/g, "PHONE_REDACTED"],
|
|
100
123
|
["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
|
|
101
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
|
|
102
126
|
["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
|
|
103
127
|
];
|
|
104
|
-
function
|
|
128
|
+
function normalize(value) {
|
|
129
|
+
return value.replace(/[\s-]/g, "").toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
var ScrubContext = class {
|
|
132
|
+
constructor(piiKey) {
|
|
133
|
+
this.piiKey = piiKey;
|
|
134
|
+
}
|
|
135
|
+
piiKey;
|
|
136
|
+
seen = /* @__PURE__ */ new Map();
|
|
137
|
+
next = /* @__PURE__ */ new Map();
|
|
138
|
+
placeholder(base, value) {
|
|
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)}`;
|
|
144
|
+
let n = this.seen.get(key);
|
|
145
|
+
if (n === void 0) {
|
|
146
|
+
n = (this.next.get(base) ?? 0) + 1;
|
|
147
|
+
this.next.set(base, n);
|
|
148
|
+
this.seen.set(key, n);
|
|
149
|
+
}
|
|
150
|
+
return `[${base}_${n}]`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function scrub(text, context = new ScrubContext()) {
|
|
105
154
|
if (!text) return { text, detections: [] };
|
|
106
155
|
const detections = [];
|
|
107
|
-
const counters = {};
|
|
108
156
|
let result = text;
|
|
109
157
|
for (const [category, pattern, basePlaceholder] of RULES) {
|
|
110
158
|
pattern.lastIndex = 0;
|
|
111
|
-
result = result.replace(pattern, () => {
|
|
112
|
-
|
|
113
|
-
const placeholder = `[${basePlaceholder}_${counters[category]}]`;
|
|
159
|
+
result = result.replace(pattern, (match) => {
|
|
160
|
+
const placeholder = context.placeholder(basePlaceholder, match);
|
|
114
161
|
detections.push({ category, placeholder });
|
|
115
162
|
return placeholder;
|
|
116
163
|
});
|
|
117
164
|
}
|
|
118
165
|
return { text: result, detections };
|
|
119
166
|
}
|
|
120
|
-
function scrubMessages(messages) {
|
|
167
|
+
function scrubMessages(messages, context = new ScrubContext()) {
|
|
121
168
|
const allDetections = [];
|
|
122
169
|
const scrubbed = [];
|
|
123
170
|
for (const msg of messages) {
|
|
124
171
|
const content = msg.content;
|
|
125
172
|
if (typeof content === "string") {
|
|
126
|
-
const result = scrub(content);
|
|
173
|
+
const result = scrub(content, context);
|
|
127
174
|
scrubbed.push({ ...msg, content: result.text });
|
|
128
175
|
allDetections.push(...result.detections);
|
|
129
176
|
} else if (Array.isArray(content)) {
|
|
130
177
|
const newBlocks = content.map((block) => {
|
|
131
178
|
if (block.type === "text" && typeof block.text === "string") {
|
|
132
|
-
const result = scrub(block.text);
|
|
179
|
+
const result = scrub(block.text, context);
|
|
133
180
|
allDetections.push(...result.detections);
|
|
134
181
|
return { ...block, text: result.text };
|
|
135
182
|
}
|
|
@@ -161,9 +208,15 @@ function observe(result, onData) {
|
|
|
161
208
|
};
|
|
162
209
|
try {
|
|
163
210
|
return new Proxy(result, {
|
|
164
|
-
get(target, prop) {
|
|
211
|
+
get(target, prop, proxy) {
|
|
165
212
|
const value = Reflect.get(target, prop, target);
|
|
166
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
|
+
}
|
|
167
220
|
if (prop === "then") {
|
|
168
221
|
return (onFulfilled, onRejected) => value.call(
|
|
169
222
|
target,
|
|
@@ -194,96 +247,200 @@ function observe(result, onData) {
|
|
|
194
247
|
function isStream(opts) {
|
|
195
248
|
return opts?.stream === true;
|
|
196
249
|
}
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
273
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const responseText = choice?.message?.content ?? "";
|
|
215
|
-
const scrubbedResponse = responseText ? scrub(responseText).text : "";
|
|
216
|
-
const usage = response?.usage;
|
|
217
|
-
enqueue({
|
|
218
|
-
model: response?.model ?? model,
|
|
219
|
-
messages: scrubbedMessages,
|
|
220
|
-
response: scrubbedResponse,
|
|
221
|
-
prompt_tokens: usage?.prompt_tokens ?? 0,
|
|
222
|
-
completion_tokens: usage?.completion_tokens ?? 0,
|
|
223
|
-
latency_ms: latencyMs,
|
|
224
|
-
timestamp: requestAt,
|
|
225
|
-
finish_reason: choice?.finish_reason ?? null,
|
|
226
|
-
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
227
|
-
});
|
|
228
|
-
} catch {
|
|
274
|
+
result = await iterator.next(...args);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
finish();
|
|
277
|
+
throw err;
|
|
229
278
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
|
232
355
|
};
|
|
233
|
-
completions[PATCHED] = true;
|
|
234
356
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
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) => {
|
|
240
361
|
const requestAt = utcnow();
|
|
241
362
|
const t0 = performance.now();
|
|
242
363
|
const result = original(opts, ...rest);
|
|
243
|
-
|
|
244
|
-
return observe(result, (response) => {
|
|
364
|
+
const done = (response) => {
|
|
245
365
|
const latencyMs = Math.round(performance.now() - t0);
|
|
246
|
-
setImmediate(() =>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const model = opts?.model ?? "";
|
|
251
|
-
const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
|
|
252
|
-
const [scrubbedMessages, detections] = scrubMessages(allMessages);
|
|
253
|
-
const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
254
|
-
const scrubbedResponse = responseText ? scrub(responseText).text : "";
|
|
255
|
-
const usage = response?.usage;
|
|
256
|
-
enqueue({
|
|
257
|
-
model: response?.model ?? model,
|
|
258
|
-
messages: scrubbedMessages,
|
|
259
|
-
response: scrubbedResponse,
|
|
260
|
-
prompt_tokens: usage?.input_tokens ?? 0,
|
|
261
|
-
completion_tokens: usage?.output_tokens ?? 0,
|
|
262
|
-
latency_ms: latencyMs,
|
|
263
|
-
timestamp: requestAt,
|
|
264
|
-
finish_reason: response?.stop_reason ?? null,
|
|
265
|
-
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
266
|
-
});
|
|
267
|
-
} catch {
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
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);
|
|
271
370
|
};
|
|
272
|
-
|
|
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 }))
|
|
394
|
+
};
|
|
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);
|
|
273
425
|
}
|
|
274
426
|
|
|
275
427
|
// src/client.ts
|
|
276
428
|
var state = {
|
|
277
429
|
apiKey: null,
|
|
278
430
|
baseUrl: "https://api.sovergate.com",
|
|
431
|
+
piiKey: void 0,
|
|
279
432
|
queue: null
|
|
280
433
|
};
|
|
281
|
-
function init({ apiKey, baseUrl = "https://api.sovergate.com", debug = false }) {
|
|
434
|
+
function init({ apiKey, baseUrl = "https://api.sovergate.com", piiKey, debug = false }) {
|
|
282
435
|
try {
|
|
283
436
|
state.apiKey = apiKey;
|
|
284
437
|
state.baseUrl = baseUrl.replace(/\/$/, "");
|
|
438
|
+
state.piiKey = piiKey || void 0;
|
|
285
439
|
if (debug) {
|
|
286
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
|
+
}
|
|
287
444
|
}
|
|
288
445
|
state.queue = new LogQueue((payload) => send(apiKey, payload, state.baseUrl));
|
|
289
446
|
} catch (err) {
|
|
@@ -299,11 +456,11 @@ function instrument(client) {
|
|
|
299
456
|
try {
|
|
300
457
|
const enqueue = state.queue.enqueue.bind(state.queue);
|
|
301
458
|
if (client?.chat?.completions) {
|
|
302
|
-
patchOpenAI(client, enqueue);
|
|
459
|
+
patchOpenAI(client, enqueue, state.piiKey);
|
|
303
460
|
return;
|
|
304
461
|
}
|
|
305
462
|
if (client?.messages) {
|
|
306
|
-
patchAnthropic(client, enqueue);
|
|
463
|
+
patchAnthropic(client, enqueue, state.piiKey);
|
|
307
464
|
return;
|
|
308
465
|
}
|
|
309
466
|
warn(
|
|
@@ -321,6 +478,7 @@ function warn(message, err) {
|
|
|
321
478
|
}
|
|
322
479
|
function shutdown() {
|
|
323
480
|
state.queue = null;
|
|
481
|
+
state.piiKey = void 0;
|
|
324
482
|
}
|
|
325
483
|
// Annotate the CommonJS export names for ESM import in node:
|
|
326
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\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<boolean> {\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 return response.status < 400\n } catch {\n return false\n }\n}\n","type Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<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 success = false\n try {\n success = await this.sendFn(this.items[0])\n } catch {\n // never propagate\n }\n\n if (success) {\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 ['phone', /\\b(?:\\+?\\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\nexport function scrub(text: string): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n const counters: Record<string, number> = {}\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, () => {\n counters[category] = (counters[category] ?? 0) + 1\n const placeholder = `[${basePlaceholder}_${counters[category]}]`\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(messages: OpenAIMessage[]): [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)\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)\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 { 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 [scrubbedMessages, detections] = scrubMessages(messages)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResponse = responseText ? scrub(responseText).text : ''\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 [scrubbedMessages, detections] = scrubMessages(allMessages)\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 scrubbedResponse = responseText ? scrub(responseText).text : ''\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/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":[]}
|