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 +40 -3
- package/dist/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +214 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -83
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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\-().
|
|
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
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
return observe(result, (response) => {
|
|
364
|
+
const done = (response) => {
|
|
269
365
|
const latencyMs = Math.round(performance.now() - t0);
|
|
270
|
-
setImmediate(() =>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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\-().
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
240
|
-
return observe(result, (response) => {
|
|
336
|
+
const done = (response) => {
|
|
241
337
|
const latencyMs = Math.round(performance.now() - t0);
|
|
242
|
-
setImmediate(() =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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,
|
package/dist/index.mjs.map
CHANGED
|
@@ -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