sovergate 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Postolovski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,14 +1,33 @@
1
- # sovergate — TypeScript SDK
1
+ # sovergate
2
2
 
3
- EU AI Act Article 12 compliance — automatic, non-invasive audit logging for LLM calls.
3
+ TypeScript / JavaScript SDK for [Sovergate](https://sovergate.com): EU AI Act
4
+ Article 12 audit logging for LLM calls, with PII scrubbed inside your own
5
+ infrastructure.
6
+
7
+ > **Early access.** The SDK is published as an alpha and the hosted ingest
8
+ > service is not generally available yet. APIs may change before 1.0.
9
+
10
+ ## How it works
11
+
12
+ Your app keeps calling OpenAI or Anthropic directly. Sovergate does not proxy
13
+ or intercept the request. After a call completes, the SDK takes a copy,
14
+ scrubs personal data **locally**, and sends the scrubbed copy to Sovergate in
15
+ the background.
16
+
17
+ ```
18
+ Your app ─────────────────────────────► OpenAI / Anthropic
19
+ │
20
+ └──► scrub PII locally ──► Sovergate (async, fire and forget)
21
+ ```
4
22
 
5
23
  ## Install
6
24
 
7
25
  ```bash
8
- npm install sovergate
26
+ npm install sovergate@alpha
9
27
  ```
10
28
 
11
- Requires Node.js 18+.
29
+ Requires Node.js 18+. Ships ESM and CommonJS builds with TypeScript types.
30
+ No runtime dependencies.
12
31
 
13
32
  ## Quick start
14
33
 
@@ -21,12 +40,12 @@ const openai = new OpenAI()
21
40
  init({ apiKey: 'sov_prod_xxxx' })
22
41
  instrument(openai)
23
42
 
24
- // Your existing code is completely unchanged
43
+ // Your existing code is unchanged
25
44
  const response = await openai.chat.completions.create({
26
45
  model: 'gpt-4o',
27
46
  messages: [{ role: 'user', content: 'Hello' }],
28
47
  })
29
- // Response returned immediately — logging fires in background
48
+ // The response is returned immediately; logging happens in the background
30
49
  ```
31
50
 
32
51
  Anthropic works the same way:
@@ -45,21 +64,67 @@ instrument(client)
45
64
  ```typescript
46
65
  init({
47
66
  apiKey: 'sov_prod_xxxx', // required
48
- baseUrl: 'https://...', // default: https://api.sovergate.com
49
- debug: true, // print SDK internals to console.debug
67
+ baseUrl: 'https://...', // default: https://api.sovergate.com
68
+ debug: true, // print SDK internals to console.debug
50
69
  })
51
70
  ```
52
71
 
72
+ ## What is sent to Sovergate
73
+
74
+ For each completed call, one JSON record:
75
+
76
+ | Field | Content |
77
+ |---|---|
78
+ | `model` | Model name returned by the provider |
79
+ | `messages` | Your prompt messages, **after** PII scrubbing |
80
+ | `response` | The model's text output, **after** PII scrubbing |
81
+ | `prompt_tokens`, `completion_tokens` | Token counts from the provider |
82
+ | `latency_ms`, `timestamp` | Call duration and start time |
83
+ | `finish_reason` | Why the model stopped |
84
+ | `pii_detections` | Category and placeholder of each scrubbed item, never the original value |
85
+
86
+ Never sent: the raw prompt or response, your LLM provider API key, or request
87
+ headers.
88
+
89
+ ### What the scrubber detects
90
+
91
+ Regex-based, applied to every text part of the prompt and response: email
92
+ addresses, IP addresses, IBANs, phone numbers, dates of birth, ID numbers, and
93
+ names that follow a title (Mr, Mrs, Ms, Dr, Prof). Other personal data, such
94
+ as names without a title, is **not** detected yet, so keep data minimisation in
95
+ mind when you build prompts. The code is in [`src/scrubber.ts`](src/scrubber.ts).
96
+
53
97
  ## Guarantees
54
98
 
55
- - Never blocks the event loop — logging runs via `setImmediate`
56
- - Never throws — if setup fails, the SDK logs a `console.warn` and stays out of the way
57
- - Leaves your client unchanged — `create()` still returns the SDK's own object, so `.withResponse()` and `.asResponse()` keep working
58
- - 2 second timeout on every HTTP call to our servers
59
- - Queues locally and retries with exponential backoff if our servers are down
60
- - PII is scrubbed **before** any data leaves your infrastructure
99
+ - Never blocks the event loop: logging runs via `setImmediate`
100
+ - Never throws: if setup fails, the SDK logs a `console.warn` and stays out of the way
101
+ - Leaves your client unchanged: `create()` still returns the provider SDK's own
102
+ object, so `.withResponse()` and `.asResponse()` keep working
103
+ - 2 second timeout on every HTTP call to Sovergate
104
+ - If Sovergate is unreachable, records are queued in memory (up to 10,000) and
105
+ retried with exponential backoff
106
+ - A record Sovergate permanently rejects (for example after a key is revoked) is
107
+ dropped with a `console.warn`, so it never blocks the records behind it
61
108
 
62
109
  ## Current limitations
63
110
 
64
111
  - Streaming calls (`stream: true`) are passed through untouched but not logged yet.
65
112
  - PII placeholders are positional (`[EMAIL_REDACTED_1]`) and not correlatable across logs.
113
+ - The retry queue is in memory, so queued records are lost if the process exits.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ npm ci
119
+ npm run typecheck
120
+ npm test
121
+ npm run build
122
+ ```
123
+
124
+ ## Security
125
+
126
+ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md).
127
+
128
+ ## License
129
+
130
+ [MIT](LICENSE)
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ module.exports = __toCommonJS(index_exports);
29
29
  // src/sender.ts
30
30
  var BASE_URL = "https://api.sovergate.com";
31
31
  var TIMEOUT_MS = 2e3;
32
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
32
33
  async function send(apiKey, payload, baseUrl = BASE_URL) {
33
34
  try {
34
35
  const controller = new AbortController();
@@ -43,9 +44,15 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
43
44
  signal: controller.signal
44
45
  });
45
46
  clearTimeout(timer);
46
- return response.status < 400;
47
+ if (response.status < 400) return "sent";
48
+ if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return "retry";
49
+ try {
50
+ console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`);
51
+ } catch {
52
+ }
53
+ return "drop";
47
54
  } catch {
48
- return false;
55
+ return "retry";
49
56
  }
50
57
  }
51
58
 
@@ -73,12 +80,13 @@ var LogQueue = class {
73
80
  this.draining = true;
74
81
  let retryIndex = 0;
75
82
  while (this.items.length > 0) {
76
- let success = false;
83
+ let done = false;
77
84
  try {
78
- success = await this.sendFn(this.items[0]);
85
+ const outcome = await this.sendFn(this.items[0]);
86
+ done = outcome === true || outcome === "sent" || outcome === "drop";
79
87
  } catch {
80
88
  }
81
- if (success) {
89
+ if (done) {
82
90
  this.items.shift();
83
91
  retryIndex = 0;
84
92
  } else {
@@ -96,40 +104,53 @@ var RULES = [
96
104
  ["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
97
105
  ["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
98
106
  ["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
99
- ["phone", /\b(?:\+?\d[\d\s\-(). ]{5,14}\d)\b/g, "PHONE_REDACTED"],
107
+ // (?<![\w+]) instead of \b so a leading "+" is part of the match, not left behind
108
+ ["phone", /(?<![\w+])\+?\d[\d\s\-(). ]{5,14}\d\b/g, "PHONE_REDACTED"],
100
109
  ["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
101
110
  ["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
102
111
  ["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
103
112
  ];
104
- function scrub(text) {
113
+ var ScrubContext = class {
114
+ seen = /* @__PURE__ */ new Map();
115
+ next = /* @__PURE__ */ new Map();
116
+ placeholder(base, value) {
117
+ const key = `${base}\0${value.replace(/[\s-]/g, "").toLowerCase()}`;
118
+ let n = this.seen.get(key);
119
+ if (n === void 0) {
120
+ n = (this.next.get(base) ?? 0) + 1;
121
+ this.next.set(base, n);
122
+ this.seen.set(key, n);
123
+ }
124
+ return `[${base}_${n}]`;
125
+ }
126
+ };
127
+ function scrub(text, context = new ScrubContext()) {
105
128
  if (!text) return { text, detections: [] };
106
129
  const detections = [];
107
- const counters = {};
108
130
  let result = text;
109
131
  for (const [category, pattern, basePlaceholder] of RULES) {
110
132
  pattern.lastIndex = 0;
111
- result = result.replace(pattern, () => {
112
- counters[category] = (counters[category] ?? 0) + 1;
113
- const placeholder = `[${basePlaceholder}_${counters[category]}]`;
133
+ result = result.replace(pattern, (match) => {
134
+ const placeholder = context.placeholder(basePlaceholder, match);
114
135
  detections.push({ category, placeholder });
115
136
  return placeholder;
116
137
  });
117
138
  }
118
139
  return { text: result, detections };
119
140
  }
120
- function scrubMessages(messages) {
141
+ function scrubMessages(messages, context = new ScrubContext()) {
121
142
  const allDetections = [];
122
143
  const scrubbed = [];
123
144
  for (const msg of messages) {
124
145
  const content = msg.content;
125
146
  if (typeof content === "string") {
126
- const result = scrub(content);
147
+ const result = scrub(content, context);
127
148
  scrubbed.push({ ...msg, content: result.text });
128
149
  allDetections.push(...result.detections);
129
150
  } else if (Array.isArray(content)) {
130
151
  const newBlocks = content.map((block) => {
131
152
  if (block.type === "text" && typeof block.text === "string") {
132
- const result = scrub(block.text);
153
+ const result = scrub(block.text, context);
133
154
  allDetections.push(...result.detections);
134
155
  return { ...block, text: result.text };
135
156
  }
@@ -209,10 +230,13 @@ function patchOpenAI(client, enqueue) {
209
230
  try {
210
231
  const messages = opts?.messages ?? [];
211
232
  const model = opts?.model ?? "";
212
- const [scrubbedMessages, detections] = scrubMessages(messages);
233
+ const context = new ScrubContext();
234
+ const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
213
235
  const choice = response?.choices?.[0];
214
236
  const responseText = choice?.message?.content ?? "";
215
- const scrubbedResponse = responseText ? scrub(responseText).text : "";
237
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
238
+ const scrubbedResponse = scrubbedResult?.text ?? "";
239
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
216
240
  const usage = response?.usage;
217
241
  enqueue({
218
242
  model: response?.model ?? model,
@@ -249,9 +273,12 @@ function patchAnthropic(client, enqueue) {
249
273
  const system = opts?.system;
250
274
  const model = opts?.model ?? "";
251
275
  const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
252
- const [scrubbedMessages, detections] = scrubMessages(allMessages);
276
+ const context = new ScrubContext();
277
+ const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
253
278
  const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
254
- const scrubbedResponse = responseText ? scrub(responseText).text : "";
279
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
280
+ const scrubbedResponse = scrubbedResult?.text ?? "";
281
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
255
282
  const usage = response?.usage;
256
283
  enqueue({
257
284
  model: response?.model ?? model,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["export { init, instrument, shutdown } from './client'\nexport type { InitOptions } from './client'\nexport type { Detection, ScrubResult } from './scrubber'\n","const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<boolean> {\n try {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n return response.status < 400\n } catch {\n return false\n }\n}\n","type Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let success = false\n try {\n success = await this.sendFn(this.items[0])\n } catch {\n // never propagate\n }\n\n if (success) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","export interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Each rule: [category, regex (must have g flag), placeholder base]\n// Order matters — IP before phone to avoid dotted-quad false matches.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n ['iban', /\\b[A-Z]{2}\\d{2}[A-Z0-9]{4,30}\\b/g, 'IBAN_REDACTED'],\n ['phone', /\\b(?:\\+?\\d[\\d\\s\\-(). ]{5,14}\\d)\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\nexport function scrub(text: string): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n const counters: Record<string, number> = {}\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, () => {\n counters[category] = (counters[category] ?? 0) + 1\n const placeholder = `[${basePlaceholder}_${counters[category]}]`\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(messages: OpenAIMessage[]): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue): void {\n const completions = client.chat.completions\n if (completions[PATCHED]) return\n const original = completions.create.bind(completions)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n completions.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const [scrubbedMessages, detections] = scrubMessages(messages)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResponse = responseText ? scrub(responseText).text : ''\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n completions[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue): void {\n const messagesApi = client.messages\n if (messagesApi[PATCHED]) return\n const original = messagesApi.create.bind(messagesApi)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messagesApi.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const [scrubbedMessages, detections] = scrubMessages(allMessages)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResponse = responseText ? scrub(responseText).text : ''\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n messagesApi[PATCHED] = true\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sender.ts","../src/queue.ts","../src/scrubber.ts","../src/patcher.ts","../src/client.ts"],"sourcesContent":["export { init, instrument, shutdown } from './client'\nexport type { InitOptions } from './client'\nexport type { Detection, ScrubResult } from './scrubber'\n","const BASE_URL = 'https://api.sovergate.com'\nconst TIMEOUT_MS = 2000\nconst RETRYABLE_STATUS = new Set([408, 425, 429])\n\n/**\n * sent: accepted by the server\n * retry: temporary failure, keep the record and try again later\n * drop: permanently rejected, retrying the same record can never succeed\n */\nexport type SendOutcome = 'sent' | 'retry' | 'drop'\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<SendOutcome> {\n 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":[]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/sender.ts
2
2
  var BASE_URL = "https://api.sovergate.com";
3
3
  var TIMEOUT_MS = 2e3;
4
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
4
5
  async function send(apiKey, payload, baseUrl = BASE_URL) {
5
6
  try {
6
7
  const controller = new AbortController();
@@ -15,9 +16,15 @@ async function send(apiKey, payload, baseUrl = BASE_URL) {
15
16
  signal: controller.signal
16
17
  });
17
18
  clearTimeout(timer);
18
- return response.status < 400;
19
+ if (response.status < 400) return "sent";
20
+ if (RETRYABLE_STATUS.has(response.status) || response.status >= 500) return "retry";
21
+ try {
22
+ console.warn(`[sovergate] ingest rejected a record (HTTP ${response.status}); dropping it`);
23
+ } catch {
24
+ }
25
+ return "drop";
19
26
  } catch {
20
- return false;
27
+ return "retry";
21
28
  }
22
29
  }
23
30
 
@@ -45,12 +52,13 @@ var LogQueue = class {
45
52
  this.draining = true;
46
53
  let retryIndex = 0;
47
54
  while (this.items.length > 0) {
48
- let success = false;
55
+ let done = false;
49
56
  try {
50
- success = await this.sendFn(this.items[0]);
57
+ const outcome = await this.sendFn(this.items[0]);
58
+ done = outcome === true || outcome === "sent" || outcome === "drop";
51
59
  } catch {
52
60
  }
53
- if (success) {
61
+ if (done) {
54
62
  this.items.shift();
55
63
  retryIndex = 0;
56
64
  } else {
@@ -68,40 +76,53 @@ var RULES = [
68
76
  ["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
69
77
  ["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
70
78
  ["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
71
- ["phone", /\b(?:\+?\d[\d\s\-(). ]{5,14}\d)\b/g, "PHONE_REDACTED"],
79
+ // (?<![\w+]) instead of \b so a leading "+" is part of the match, not left behind
80
+ ["phone", /(?<![\w+])\+?\d[\d\s\-(). ]{5,14}\d\b/g, "PHONE_REDACTED"],
72
81
  ["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
73
82
  ["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
74
83
  ["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
75
84
  ];
76
- function scrub(text) {
85
+ var ScrubContext = class {
86
+ seen = /* @__PURE__ */ new Map();
87
+ next = /* @__PURE__ */ new Map();
88
+ placeholder(base, value) {
89
+ const key = `${base}\0${value.replace(/[\s-]/g, "").toLowerCase()}`;
90
+ let n = this.seen.get(key);
91
+ if (n === void 0) {
92
+ n = (this.next.get(base) ?? 0) + 1;
93
+ this.next.set(base, n);
94
+ this.seen.set(key, n);
95
+ }
96
+ return `[${base}_${n}]`;
97
+ }
98
+ };
99
+ function scrub(text, context = new ScrubContext()) {
77
100
  if (!text) return { text, detections: [] };
78
101
  const detections = [];
79
- const counters = {};
80
102
  let result = text;
81
103
  for (const [category, pattern, basePlaceholder] of RULES) {
82
104
  pattern.lastIndex = 0;
83
- result = result.replace(pattern, () => {
84
- counters[category] = (counters[category] ?? 0) + 1;
85
- const placeholder = `[${basePlaceholder}_${counters[category]}]`;
105
+ result = result.replace(pattern, (match) => {
106
+ const placeholder = context.placeholder(basePlaceholder, match);
86
107
  detections.push({ category, placeholder });
87
108
  return placeholder;
88
109
  });
89
110
  }
90
111
  return { text: result, detections };
91
112
  }
92
- function scrubMessages(messages) {
113
+ function scrubMessages(messages, context = new ScrubContext()) {
93
114
  const allDetections = [];
94
115
  const scrubbed = [];
95
116
  for (const msg of messages) {
96
117
  const content = msg.content;
97
118
  if (typeof content === "string") {
98
- const result = scrub(content);
119
+ const result = scrub(content, context);
99
120
  scrubbed.push({ ...msg, content: result.text });
100
121
  allDetections.push(...result.detections);
101
122
  } else if (Array.isArray(content)) {
102
123
  const newBlocks = content.map((block) => {
103
124
  if (block.type === "text" && typeof block.text === "string") {
104
- const result = scrub(block.text);
125
+ const result = scrub(block.text, context);
105
126
  allDetections.push(...result.detections);
106
127
  return { ...block, text: result.text };
107
128
  }
@@ -181,10 +202,13 @@ function patchOpenAI(client, enqueue) {
181
202
  try {
182
203
  const messages = opts?.messages ?? [];
183
204
  const model = opts?.model ?? "";
184
- const [scrubbedMessages, detections] = scrubMessages(messages);
205
+ const context = new ScrubContext();
206
+ const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
185
207
  const choice = response?.choices?.[0];
186
208
  const responseText = choice?.message?.content ?? "";
187
- const scrubbedResponse = responseText ? scrub(responseText).text : "";
209
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
210
+ const scrubbedResponse = scrubbedResult?.text ?? "";
211
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
188
212
  const usage = response?.usage;
189
213
  enqueue({
190
214
  model: response?.model ?? model,
@@ -221,9 +245,12 @@ function patchAnthropic(client, enqueue) {
221
245
  const system = opts?.system;
222
246
  const model = opts?.model ?? "";
223
247
  const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
224
- const [scrubbedMessages, detections] = scrubMessages(allMessages);
248
+ const context = new ScrubContext();
249
+ const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
225
250
  const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
226
- const scrubbedResponse = responseText ? scrub(responseText).text : "";
251
+ const scrubbedResult = responseText ? scrub(responseText, context) : null;
252
+ const scrubbedResponse = scrubbedResult?.text ?? "";
253
+ const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
227
254
  const usage = response?.usage;
228
255
  enqueue({
229
256
  model: response?.model ?? model,
@@ -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\n\nexport async function send(\n apiKey: string,\n payload: Record<string, unknown>,\n baseUrl = BASE_URL,\n): Promise<boolean> {\n try {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n\n const response = await fetch(`${baseUrl}/ingest`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n return response.status < 400\n } catch {\n return false\n }\n}\n","type Payload = Record<string, unknown>\ntype SendFn = (payload: Payload) => Promise<boolean>\n\nconst MAX_QUEUE = 10_000\nconst RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60]\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nexport class LogQueue {\n private readonly sendFn: SendFn\n private readonly items: Payload[] = []\n private draining = false\n\n constructor(sendFn: SendFn) {\n this.sendFn = sendFn\n }\n\n enqueue(payload: Payload): void {\n if (this.items.length >= MAX_QUEUE) return\n this.items.push(payload)\n if (!this.draining) {\n setImmediate(() => void this.drain())\n }\n }\n\n private async drain(): Promise<void> {\n this.draining = true\n let retryIndex = 0\n\n while (this.items.length > 0) {\n let success = false\n try {\n success = await this.sendFn(this.items[0])\n } catch {\n // never propagate\n }\n\n if (success) {\n this.items.shift()\n retryIndex = 0\n } else {\n const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)]\n retryIndex++\n await sleep(delaySec * 1000)\n }\n }\n\n this.draining = false\n }\n}\n","export interface Detection {\n category: string\n placeholder: string\n}\n\nexport interface ScrubResult {\n text: string\n detections: Detection[]\n}\n\ntype ContentBlock = { type: string; text?: string } & Record<string, unknown>\nexport type OpenAIMessage = { role: string; content: string | ContentBlock[] } & Record<string, unknown>\n\n// Each rule: [category, regex (must have g flag), placeholder base]\n// Order matters — IP before phone to avoid dotted-quad false matches.\nconst RULES: Array<[string, RegExp, string]> = [\n ['email', /\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b/g, 'EMAIL_REDACTED'],\n ['ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, 'IP_REDACTED'],\n ['iban', /\\b[A-Z]{2}\\d{2}[A-Z0-9]{4,30}\\b/g, 'IBAN_REDACTED'],\n ['phone', /\\b(?:\\+?\\d[\\d\\s\\-(). ]{5,14}\\d)\\b/g, 'PHONE_REDACTED'],\n ['dob', /\\b(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4}|\\d{4}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{1,2})\\b/g, 'DOB_REDACTED'],\n ['id_number', /\\b[A-Z]{1,3}\\d{6,9}\\b/g, 'ID_REDACTED'],\n ['name', /\\b(?:Mr|Mrs|Ms|Dr|Prof)\\.?\\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\\b/g, 'NAME_REDACTED'],\n]\n\nexport function scrub(text: string): ScrubResult {\n if (!text) return { text, detections: [] }\n\n const detections: Detection[] = []\n const counters: Record<string, number> = {}\n\n let result = text\n for (const [category, pattern, basePlaceholder] of RULES) {\n pattern.lastIndex = 0\n result = result.replace(pattern, () => {\n counters[category] = (counters[category] ?? 0) + 1\n const placeholder = `[${basePlaceholder}_${counters[category]}]`\n detections.push({ category, placeholder })\n return placeholder\n })\n }\n\n return { text: result, detections }\n}\n\nexport function scrubMessages(messages: OpenAIMessage[]): [OpenAIMessage[], Detection[]] {\n const allDetections: Detection[] = []\n const scrubbed: OpenAIMessage[] = []\n\n for (const msg of messages) {\n const content = msg.content\n if (typeof content === 'string') {\n const result = scrub(content)\n scrubbed.push({ ...msg, content: result.text })\n allDetections.push(...result.detections)\n } else if (Array.isArray(content)) {\n const newBlocks = content.map(block => {\n if (block.type === 'text' && typeof block.text === 'string') {\n const result = scrub(block.text)\n allDetections.push(...result.detections)\n return { ...block, text: result.text }\n }\n return block\n })\n scrubbed.push({ ...msg, content: newBlocks })\n } else {\n scrubbed.push(msg)\n }\n }\n\n return [scrubbed, allDetections]\n}\n","import { scrub, scrubMessages } from './scrubber'\n\ntype Enqueue = (payload: Record<string, unknown>) => void\n\nconst PATCHED = Symbol.for('sovergate.patched')\n\nfunction utcnow(): string {\n return new Date().toISOString()\n}\n\n/**\n * Returns the provider's own result unchanged in behaviour, but notifies `onData`\n * the first time the app reads the parsed result (await / .then / .withResponse).\n *\n * The OpenAI and Anthropic SDKs return an APIPromise with extra methods\n * (.withResponse(), .asResponse()) and parse the body lazily. Wrapping create()\n * in an async function would drop those methods, and reading the result eagerly\n * would consume the body before an app calling .asResponse() can. A Proxy keeps\n * the original object, its prototype and its laziness intact.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction observe(result: any, onData: (data: any) => void): any {\n if (!result || typeof result.then !== 'function') return result\n\n let seen = false\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const notify = (data: any): void => {\n if (seen) return\n seen = true\n try {\n onData(data)\n } catch {\n // never let logging errors surface to the caller\n }\n }\n\n try {\n return new Proxy(result, {\n get(target, prop) {\n const value = Reflect.get(target, prop, target)\n if (typeof value !== 'function') return value\n\n if (prop === 'then') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (onFulfilled?: any, onRejected?: any) =>\n value.call(\n target,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (data: any) => {\n notify(data)\n return typeof onFulfilled === 'function' ? onFulfilled(data) : data\n },\n onRejected,\n )\n }\n\n if (prop === 'withResponse') {\n return (...args: unknown[]) =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n value.apply(target, args).then((withResponse: any) => {\n notify(withResponse?.data)\n return withResponse\n })\n }\n\n return value.bind(target)\n },\n })\n } catch {\n return result\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isStream(opts: any): boolean {\n return opts?.stream === true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchOpenAI(client: any, enqueue: Enqueue): void {\n const completions = client.chat.completions\n if (completions[PATCHED]) return\n const original = completions.create.bind(completions)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n completions.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const model = opts?.model ?? ''\n const [scrubbedMessages, detections] = scrubMessages(messages)\n const choice = response?.choices?.[0]\n const responseText: string = choice?.message?.content ?? ''\n const scrubbedResponse = responseText ? scrub(responseText).text : ''\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.prompt_tokens ?? 0,\n completion_tokens: usage?.completion_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: choice?.finish_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n completions[PATCHED] = true\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function patchAnthropic(client: any, enqueue: Enqueue): void {\n const messagesApi = client.messages\n if (messagesApi[PATCHED]) return\n const original = messagesApi.create.bind(messagesApi)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messagesApi.create = (opts: any, ...rest: unknown[]) => {\n const requestAt = utcnow()\n const t0 = performance.now()\n\n const result = original(opts, ...rest)\n\n // Streams can only be read once and belong to the app; they are not logged yet.\n if (isStream(opts)) return result\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return observe(result, (response: any) => {\n const latencyMs = Math.round(performance.now() - t0)\n\n setImmediate(() => {\n try {\n const messages = opts?.messages ?? []\n const system = opts?.system\n const model = opts?.model ?? ''\n\n const allMessages = system\n ? [{ role: 'system', content: system }, ...messages]\n : messages\n\n const [scrubbedMessages, detections] = scrubMessages(allMessages)\n\n const responseText: string = (response?.content ?? [])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .filter((b: any) => b.type === 'text')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n .map((b: any) => b.text as string)\n .join('')\n\n const scrubbedResponse = responseText ? scrub(responseText).text : ''\n const usage = response?.usage\n\n enqueue({\n model: response?.model ?? model,\n messages: scrubbedMessages,\n response: scrubbedResponse,\n prompt_tokens: usage?.input_tokens ?? 0,\n completion_tokens: usage?.output_tokens ?? 0,\n latency_ms: latencyMs,\n timestamp: requestAt,\n finish_reason: response?.stop_reason ?? null,\n pii_detections: detections.map(d => ({ category: d.category, placeholder: d.placeholder })),\n })\n } catch {\n // never let logging errors surface to the caller\n }\n })\n })\n }\n messagesApi[PATCHED] = true\n}\n","import { send } from './sender'\nimport { LogQueue } from './queue'\nimport { patchOpenAI, patchAnthropic } from './patcher'\n\nexport interface InitOptions {\n apiKey: string\n baseUrl?: string\n debug?: boolean\n}\n\nconst state: {\n apiKey: string | null\n baseUrl: string\n queue: LogQueue | null\n} = {\n apiKey: null,\n baseUrl: 'https://api.sovergate.com',\n queue: null,\n}\n\nexport function init({ apiKey, baseUrl = 'https://api.sovergate.com', debug = false }: InitOptions): void {\n try {\n state.apiKey = apiKey\n state.baseUrl = baseUrl.replace(/\\/$/, '')\n\n if (debug) {\n console.debug('[sovergate] init baseUrl=%s', state.baseUrl)\n }\n\n state.queue = new LogQueue(payload => send(apiKey, payload, state.baseUrl))\n } catch (err) {\n // Never let SDK setup break the host app; logging just stays disabled.\n state.queue = null\n warn('init failed, logging disabled', err)\n }\n}\n\n// Never throws: if instrumentation isn't possible, warn and leave the client untouched.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrument(client: any): void {\n if (!state.queue) {\n warn('instrument() called before init({ apiKey }) — LLM calls will not be logged')\n return\n }\n\n try {\n const enqueue = state.queue.enqueue.bind(state.queue)\n\n if (client?.chat?.completions) {\n patchOpenAI(client, enqueue)\n return\n }\n\n if (client?.messages) {\n patchAnthropic(client, enqueue)\n return\n }\n\n warn(\n 'unsupported client — LLM calls will not be logged. Pass an OpenAI or Anthropic ' +\n 'client instance, or open an issue at https://github.com/sovergate/sdk-js/issues',\n )\n } catch (err) {\n warn('instrument() failed, LLM calls will not be logged', err)\n }\n}\n\nfunction warn(message: string, err?: unknown): void {\n try {\n console.warn(`[sovergate] ${message}`, ...(err === undefined ? [] : [err]))\n } catch {\n // never propagate\n }\n}\n\nexport function shutdown(): void {\n state.queue = null\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sovergate",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "EU AI Act compliance SDK — automatic, non-invasive audit logging for LLM calls",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -34,9 +34,22 @@
34
34
  "compliance",
35
35
  "gdpr",
36
36
  "llm",
37
- "audit"
37
+ "audit",
38
+ "openai",
39
+ "anthropic",
40
+ "pii",
41
+ "observability"
38
42
  ],
39
43
  "license": "MIT",
44
+ "author": "Roberto Postolovski",
45
+ "homepage": "https://sovergate.com",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/sovergate/sdk-js.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/sovergate/sdk-js/issues"
52
+ },
40
53
  "engines": {
41
54
  "node": ">=18"
42
55
  },