sovergate 0.0.0-stage → 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 +21 -0
- package/README.md +129 -2
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +329 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -4
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,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sovergate
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install sovergate@alpha
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node.js 18+. Ships ESM and CommonJS builds with TypeScript types.
|
|
30
|
+
No runtime dependencies.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import OpenAI from 'openai'
|
|
36
|
+
import { init, instrument } from 'sovergate'
|
|
37
|
+
|
|
38
|
+
const openai = new OpenAI()
|
|
39
|
+
|
|
40
|
+
init({ apiKey: 'sov_prod_xxxx' })
|
|
41
|
+
instrument(openai)
|
|
42
|
+
|
|
43
|
+
// Your existing code is unchanged
|
|
44
|
+
const response = await openai.chat.completions.create({
|
|
45
|
+
model: 'gpt-4o',
|
|
46
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
47
|
+
})
|
|
48
|
+
// The response is returned immediately; logging happens in the background
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Anthropic works the same way:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
55
|
+
import { init, instrument } from 'sovergate'
|
|
56
|
+
|
|
57
|
+
const client = new Anthropic()
|
|
58
|
+
init({ apiKey: 'sov_prod_xxxx' })
|
|
59
|
+
instrument(client)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
init({
|
|
66
|
+
apiKey: 'sov_prod_xxxx', // required
|
|
67
|
+
baseUrl: 'https://...', // default: https://api.sovergate.com
|
|
68
|
+
debug: true, // print SDK internals to console.debug
|
|
69
|
+
})
|
|
70
|
+
```
|
|
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
|
+
|
|
97
|
+
## Guarantees
|
|
98
|
+
|
|
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
|
|
108
|
+
|
|
109
|
+
## Current limitations
|
|
110
|
+
|
|
111
|
+
- Streaming calls (`stream: true`) are passed through untouched but not logged yet.
|
|
112
|
+
- PII placeholders are positional (`[EMAIL_REDACTED_1]`) and not correlatable across logs.
|
|
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.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface InitOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare function init({ apiKey, baseUrl, debug }: InitOptions): void;
|
|
7
|
+
declare function instrument(client: any): void;
|
|
8
|
+
declare function shutdown(): void;
|
|
9
|
+
|
|
10
|
+
interface Detection {
|
|
11
|
+
category: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
}
|
|
14
|
+
interface ScrubResult {
|
|
15
|
+
text: string;
|
|
16
|
+
detections: Detection[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { type Detection, type InitOptions, type ScrubResult, init, instrument, shutdown };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface InitOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare function init({ apiKey, baseUrl, debug }: InitOptions): void;
|
|
7
|
+
declare function instrument(client: any): void;
|
|
8
|
+
declare function shutdown(): void;
|
|
9
|
+
|
|
10
|
+
interface Detection {
|
|
11
|
+
category: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
}
|
|
14
|
+
interface ScrubResult {
|
|
15
|
+
text: string;
|
|
16
|
+
detections: Detection[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { type Detection, type InitOptions, type ScrubResult, init, instrument, shutdown };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
init: () => init,
|
|
24
|
+
instrument: () => instrument,
|
|
25
|
+
shutdown: () => shutdown
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/sender.ts
|
|
30
|
+
var BASE_URL = "https://api.sovergate.com";
|
|
31
|
+
var TIMEOUT_MS = 2e3;
|
|
32
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
33
|
+
async function send(apiKey, payload, baseUrl = BASE_URL) {
|
|
34
|
+
try {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
37
|
+
const response = await fetch(`${baseUrl}/ingest`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${apiKey}`,
|
|
41
|
+
"Content-Type": "application/json"
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(payload),
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
});
|
|
46
|
+
clearTimeout(timer);
|
|
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";
|
|
54
|
+
} catch {
|
|
55
|
+
return "retry";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/queue.ts
|
|
60
|
+
var MAX_QUEUE = 1e4;
|
|
61
|
+
var RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60];
|
|
62
|
+
function sleep(ms) {
|
|
63
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
}
|
|
65
|
+
var LogQueue = class {
|
|
66
|
+
sendFn;
|
|
67
|
+
items = [];
|
|
68
|
+
draining = false;
|
|
69
|
+
constructor(sendFn) {
|
|
70
|
+
this.sendFn = sendFn;
|
|
71
|
+
}
|
|
72
|
+
enqueue(payload) {
|
|
73
|
+
if (this.items.length >= MAX_QUEUE) return;
|
|
74
|
+
this.items.push(payload);
|
|
75
|
+
if (!this.draining) {
|
|
76
|
+
setImmediate(() => void this.drain());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async drain() {
|
|
80
|
+
this.draining = true;
|
|
81
|
+
let retryIndex = 0;
|
|
82
|
+
while (this.items.length > 0) {
|
|
83
|
+
let done = false;
|
|
84
|
+
try {
|
|
85
|
+
const outcome = await this.sendFn(this.items[0]);
|
|
86
|
+
done = outcome === true || outcome === "sent" || outcome === "drop";
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
if (done) {
|
|
90
|
+
this.items.shift();
|
|
91
|
+
retryIndex = 0;
|
|
92
|
+
} else {
|
|
93
|
+
const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)];
|
|
94
|
+
retryIndex++;
|
|
95
|
+
await sleep(delaySec * 1e3);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.draining = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/scrubber.ts
|
|
103
|
+
var RULES = [
|
|
104
|
+
["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
|
|
105
|
+
["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
|
|
106
|
+
["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
|
|
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"],
|
|
109
|
+
["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
|
|
110
|
+
["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
|
|
111
|
+
["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
|
|
112
|
+
];
|
|
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()) {
|
|
128
|
+
if (!text) return { text, detections: [] };
|
|
129
|
+
const detections = [];
|
|
130
|
+
let result = text;
|
|
131
|
+
for (const [category, pattern, basePlaceholder] of RULES) {
|
|
132
|
+
pattern.lastIndex = 0;
|
|
133
|
+
result = result.replace(pattern, (match) => {
|
|
134
|
+
const placeholder = context.placeholder(basePlaceholder, match);
|
|
135
|
+
detections.push({ category, placeholder });
|
|
136
|
+
return placeholder;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return { text: result, detections };
|
|
140
|
+
}
|
|
141
|
+
function scrubMessages(messages, context = new ScrubContext()) {
|
|
142
|
+
const allDetections = [];
|
|
143
|
+
const scrubbed = [];
|
|
144
|
+
for (const msg of messages) {
|
|
145
|
+
const content = msg.content;
|
|
146
|
+
if (typeof content === "string") {
|
|
147
|
+
const result = scrub(content, context);
|
|
148
|
+
scrubbed.push({ ...msg, content: result.text });
|
|
149
|
+
allDetections.push(...result.detections);
|
|
150
|
+
} else if (Array.isArray(content)) {
|
|
151
|
+
const newBlocks = content.map((block) => {
|
|
152
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
153
|
+
const result = scrub(block.text, context);
|
|
154
|
+
allDetections.push(...result.detections);
|
|
155
|
+
return { ...block, text: result.text };
|
|
156
|
+
}
|
|
157
|
+
return block;
|
|
158
|
+
});
|
|
159
|
+
scrubbed.push({ ...msg, content: newBlocks });
|
|
160
|
+
} else {
|
|
161
|
+
scrubbed.push(msg);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [scrubbed, allDetections];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/patcher.ts
|
|
168
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("sovergate.patched");
|
|
169
|
+
function utcnow() {
|
|
170
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
+
}
|
|
172
|
+
function observe(result, onData) {
|
|
173
|
+
if (!result || typeof result.then !== "function") return result;
|
|
174
|
+
let seen = false;
|
|
175
|
+
const notify = (data) => {
|
|
176
|
+
if (seen) return;
|
|
177
|
+
seen = true;
|
|
178
|
+
try {
|
|
179
|
+
onData(data);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
return new Proxy(result, {
|
|
185
|
+
get(target, prop) {
|
|
186
|
+
const value = Reflect.get(target, prop, target);
|
|
187
|
+
if (typeof value !== "function") return value;
|
|
188
|
+
if (prop === "then") {
|
|
189
|
+
return (onFulfilled, onRejected) => value.call(
|
|
190
|
+
target,
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
192
|
+
(data) => {
|
|
193
|
+
notify(data);
|
|
194
|
+
return typeof onFulfilled === "function" ? onFulfilled(data) : data;
|
|
195
|
+
},
|
|
196
|
+
onRejected
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (prop === "withResponse") {
|
|
200
|
+
return (...args) => (
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
+
value.apply(target, args).then((withResponse) => {
|
|
203
|
+
notify(withResponse?.data);
|
|
204
|
+
return withResponse;
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return value.bind(target);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function isStream(opts) {
|
|
216
|
+
return opts?.stream === true;
|
|
217
|
+
}
|
|
218
|
+
function patchOpenAI(client, enqueue) {
|
|
219
|
+
const completions = client.chat.completions;
|
|
220
|
+
if (completions[PATCHED]) return;
|
|
221
|
+
const original = completions.create.bind(completions);
|
|
222
|
+
completions.create = (opts, ...rest) => {
|
|
223
|
+
const requestAt = utcnow();
|
|
224
|
+
const t0 = performance.now();
|
|
225
|
+
const result = original(opts, ...rest);
|
|
226
|
+
if (isStream(opts)) return result;
|
|
227
|
+
return observe(result, (response) => {
|
|
228
|
+
const latencyMs = Math.round(performance.now() - t0);
|
|
229
|
+
setImmediate(() => {
|
|
230
|
+
try {
|
|
231
|
+
const messages = opts?.messages ?? [];
|
|
232
|
+
const model = opts?.model ?? "";
|
|
233
|
+
const context = new ScrubContext();
|
|
234
|
+
const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
|
|
235
|
+
const choice = response?.choices?.[0];
|
|
236
|
+
const responseText = choice?.message?.content ?? "";
|
|
237
|
+
const scrubbedResult = responseText ? scrub(responseText, context) : null;
|
|
238
|
+
const scrubbedResponse = scrubbedResult?.text ?? "";
|
|
239
|
+
const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
|
|
240
|
+
const usage = response?.usage;
|
|
241
|
+
enqueue({
|
|
242
|
+
model: response?.model ?? model,
|
|
243
|
+
messages: scrubbedMessages,
|
|
244
|
+
response: scrubbedResponse,
|
|
245
|
+
prompt_tokens: usage?.prompt_tokens ?? 0,
|
|
246
|
+
completion_tokens: usage?.completion_tokens ?? 0,
|
|
247
|
+
latency_ms: latencyMs,
|
|
248
|
+
timestamp: requestAt,
|
|
249
|
+
finish_reason: choice?.finish_reason ?? null,
|
|
250
|
+
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
completions[PATCHED] = true;
|
|
258
|
+
}
|
|
259
|
+
function patchAnthropic(client, enqueue) {
|
|
260
|
+
const messagesApi = client.messages;
|
|
261
|
+
if (messagesApi[PATCHED]) return;
|
|
262
|
+
const original = messagesApi.create.bind(messagesApi);
|
|
263
|
+
messagesApi.create = (opts, ...rest) => {
|
|
264
|
+
const requestAt = utcnow();
|
|
265
|
+
const t0 = performance.now();
|
|
266
|
+
const result = original(opts, ...rest);
|
|
267
|
+
if (isStream(opts)) return result;
|
|
268
|
+
return observe(result, (response) => {
|
|
269
|
+
const latencyMs = Math.round(performance.now() - t0);
|
|
270
|
+
setImmediate(() => {
|
|
271
|
+
try {
|
|
272
|
+
const messages = opts?.messages ?? [];
|
|
273
|
+
const system = opts?.system;
|
|
274
|
+
const model = opts?.model ?? "";
|
|
275
|
+
const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
|
|
276
|
+
const context = new ScrubContext();
|
|
277
|
+
const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
|
|
278
|
+
const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
279
|
+
const scrubbedResult = responseText ? scrub(responseText, context) : null;
|
|
280
|
+
const scrubbedResponse = scrubbedResult?.text ?? "";
|
|
281
|
+
const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
|
|
282
|
+
const usage = response?.usage;
|
|
283
|
+
enqueue({
|
|
284
|
+
model: response?.model ?? model,
|
|
285
|
+
messages: scrubbedMessages,
|
|
286
|
+
response: scrubbedResponse,
|
|
287
|
+
prompt_tokens: usage?.input_tokens ?? 0,
|
|
288
|
+
completion_tokens: usage?.output_tokens ?? 0,
|
|
289
|
+
latency_ms: latencyMs,
|
|
290
|
+
timestamp: requestAt,
|
|
291
|
+
finish_reason: response?.stop_reason ?? null,
|
|
292
|
+
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
messagesApi[PATCHED] = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/client.ts
|
|
303
|
+
var state = {
|
|
304
|
+
apiKey: null,
|
|
305
|
+
baseUrl: "https://api.sovergate.com",
|
|
306
|
+
queue: null
|
|
307
|
+
};
|
|
308
|
+
function init({ apiKey, baseUrl = "https://api.sovergate.com", debug = false }) {
|
|
309
|
+
try {
|
|
310
|
+
state.apiKey = apiKey;
|
|
311
|
+
state.baseUrl = baseUrl.replace(/\/$/, "");
|
|
312
|
+
if (debug) {
|
|
313
|
+
console.debug("[sovergate] init baseUrl=%s", state.baseUrl);
|
|
314
|
+
}
|
|
315
|
+
state.queue = new LogQueue((payload) => send(apiKey, payload, state.baseUrl));
|
|
316
|
+
} catch (err) {
|
|
317
|
+
state.queue = null;
|
|
318
|
+
warn("init failed, logging disabled", err);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function instrument(client) {
|
|
322
|
+
if (!state.queue) {
|
|
323
|
+
warn("instrument() called before init({ apiKey }) \u2014 LLM calls will not be logged");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const enqueue = state.queue.enqueue.bind(state.queue);
|
|
328
|
+
if (client?.chat?.completions) {
|
|
329
|
+
patchOpenAI(client, enqueue);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (client?.messages) {
|
|
333
|
+
patchAnthropic(client, enqueue);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
warn(
|
|
337
|
+
"unsupported client \u2014 LLM calls will not be logged. Pass an OpenAI or Anthropic client instance, or open an issue at https://github.com/sovergate/sdk-js/issues"
|
|
338
|
+
);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
warn("instrument() failed, LLM calls will not be logged", err);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function warn(message, err) {
|
|
344
|
+
try {
|
|
345
|
+
console.warn(`[sovergate] ${message}`, ...err === void 0 ? [] : [err]);
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function shutdown() {
|
|
350
|
+
state.queue = null;
|
|
351
|
+
}
|
|
352
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
353
|
+
0 && (module.exports = {
|
|
354
|
+
init,
|
|
355
|
+
instrument,
|
|
356
|
+
shutdown
|
|
357
|
+
});
|
|
358
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// src/sender.ts
|
|
2
|
+
var BASE_URL = "https://api.sovergate.com";
|
|
3
|
+
var TIMEOUT_MS = 2e3;
|
|
4
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
5
|
+
async function send(apiKey, payload, baseUrl = BASE_URL) {
|
|
6
|
+
try {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
9
|
+
const response = await fetch(`${baseUrl}/ingest`, {
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13
|
+
"Content-Type": "application/json"
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify(payload),
|
|
16
|
+
signal: controller.signal
|
|
17
|
+
});
|
|
18
|
+
clearTimeout(timer);
|
|
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";
|
|
26
|
+
} catch {
|
|
27
|
+
return "retry";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/queue.ts
|
|
32
|
+
var MAX_QUEUE = 1e4;
|
|
33
|
+
var RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30, 60];
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
var LogQueue = class {
|
|
38
|
+
sendFn;
|
|
39
|
+
items = [];
|
|
40
|
+
draining = false;
|
|
41
|
+
constructor(sendFn) {
|
|
42
|
+
this.sendFn = sendFn;
|
|
43
|
+
}
|
|
44
|
+
enqueue(payload) {
|
|
45
|
+
if (this.items.length >= MAX_QUEUE) return;
|
|
46
|
+
this.items.push(payload);
|
|
47
|
+
if (!this.draining) {
|
|
48
|
+
setImmediate(() => void this.drain());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async drain() {
|
|
52
|
+
this.draining = true;
|
|
53
|
+
let retryIndex = 0;
|
|
54
|
+
while (this.items.length > 0) {
|
|
55
|
+
let done = false;
|
|
56
|
+
try {
|
|
57
|
+
const outcome = await this.sendFn(this.items[0]);
|
|
58
|
+
done = outcome === true || outcome === "sent" || outcome === "drop";
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
if (done) {
|
|
62
|
+
this.items.shift();
|
|
63
|
+
retryIndex = 0;
|
|
64
|
+
} else {
|
|
65
|
+
const delaySec = RETRY_DELAYS_S[Math.min(retryIndex, RETRY_DELAYS_S.length - 1)];
|
|
66
|
+
retryIndex++;
|
|
67
|
+
await sleep(delaySec * 1e3);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.draining = false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/scrubber.ts
|
|
75
|
+
var RULES = [
|
|
76
|
+
["email", /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, "EMAIL_REDACTED"],
|
|
77
|
+
["ip", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "IP_REDACTED"],
|
|
78
|
+
["iban", /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g, "IBAN_REDACTED"],
|
|
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"],
|
|
81
|
+
["dob", /\b(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}|\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})\b/g, "DOB_REDACTED"],
|
|
82
|
+
["id_number", /\b[A-Z]{1,3}\d{6,9}\b/g, "ID_REDACTED"],
|
|
83
|
+
["name", /\b(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+(?: [A-Z][a-z]+)*\b/g, "NAME_REDACTED"]
|
|
84
|
+
];
|
|
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()) {
|
|
100
|
+
if (!text) return { text, detections: [] };
|
|
101
|
+
const detections = [];
|
|
102
|
+
let result = text;
|
|
103
|
+
for (const [category, pattern, basePlaceholder] of RULES) {
|
|
104
|
+
pattern.lastIndex = 0;
|
|
105
|
+
result = result.replace(pattern, (match) => {
|
|
106
|
+
const placeholder = context.placeholder(basePlaceholder, match);
|
|
107
|
+
detections.push({ category, placeholder });
|
|
108
|
+
return placeholder;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { text: result, detections };
|
|
112
|
+
}
|
|
113
|
+
function scrubMessages(messages, context = new ScrubContext()) {
|
|
114
|
+
const allDetections = [];
|
|
115
|
+
const scrubbed = [];
|
|
116
|
+
for (const msg of messages) {
|
|
117
|
+
const content = msg.content;
|
|
118
|
+
if (typeof content === "string") {
|
|
119
|
+
const result = scrub(content, context);
|
|
120
|
+
scrubbed.push({ ...msg, content: result.text });
|
|
121
|
+
allDetections.push(...result.detections);
|
|
122
|
+
} else if (Array.isArray(content)) {
|
|
123
|
+
const newBlocks = content.map((block) => {
|
|
124
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
125
|
+
const result = scrub(block.text, context);
|
|
126
|
+
allDetections.push(...result.detections);
|
|
127
|
+
return { ...block, text: result.text };
|
|
128
|
+
}
|
|
129
|
+
return block;
|
|
130
|
+
});
|
|
131
|
+
scrubbed.push({ ...msg, content: newBlocks });
|
|
132
|
+
} else {
|
|
133
|
+
scrubbed.push(msg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [scrubbed, allDetections];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/patcher.ts
|
|
140
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("sovergate.patched");
|
|
141
|
+
function utcnow() {
|
|
142
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
143
|
+
}
|
|
144
|
+
function observe(result, onData) {
|
|
145
|
+
if (!result || typeof result.then !== "function") return result;
|
|
146
|
+
let seen = false;
|
|
147
|
+
const notify = (data) => {
|
|
148
|
+
if (seen) return;
|
|
149
|
+
seen = true;
|
|
150
|
+
try {
|
|
151
|
+
onData(data);
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
return new Proxy(result, {
|
|
157
|
+
get(target, prop) {
|
|
158
|
+
const value = Reflect.get(target, prop, target);
|
|
159
|
+
if (typeof value !== "function") return value;
|
|
160
|
+
if (prop === "then") {
|
|
161
|
+
return (onFulfilled, onRejected) => value.call(
|
|
162
|
+
target,
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
(data) => {
|
|
165
|
+
notify(data);
|
|
166
|
+
return typeof onFulfilled === "function" ? onFulfilled(data) : data;
|
|
167
|
+
},
|
|
168
|
+
onRejected
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (prop === "withResponse") {
|
|
172
|
+
return (...args) => (
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
+
value.apply(target, args).then((withResponse) => {
|
|
175
|
+
notify(withResponse?.data);
|
|
176
|
+
return withResponse;
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return value.bind(target);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function isStream(opts) {
|
|
188
|
+
return opts?.stream === true;
|
|
189
|
+
}
|
|
190
|
+
function patchOpenAI(client, enqueue) {
|
|
191
|
+
const completions = client.chat.completions;
|
|
192
|
+
if (completions[PATCHED]) return;
|
|
193
|
+
const original = completions.create.bind(completions);
|
|
194
|
+
completions.create = (opts, ...rest) => {
|
|
195
|
+
const requestAt = utcnow();
|
|
196
|
+
const t0 = performance.now();
|
|
197
|
+
const result = original(opts, ...rest);
|
|
198
|
+
if (isStream(opts)) return result;
|
|
199
|
+
return observe(result, (response) => {
|
|
200
|
+
const latencyMs = Math.round(performance.now() - t0);
|
|
201
|
+
setImmediate(() => {
|
|
202
|
+
try {
|
|
203
|
+
const messages = opts?.messages ?? [];
|
|
204
|
+
const model = opts?.model ?? "";
|
|
205
|
+
const context = new ScrubContext();
|
|
206
|
+
const [scrubbedMessages, promptDetections] = scrubMessages(messages, context);
|
|
207
|
+
const choice = response?.choices?.[0];
|
|
208
|
+
const responseText = choice?.message?.content ?? "";
|
|
209
|
+
const scrubbedResult = responseText ? scrub(responseText, context) : null;
|
|
210
|
+
const scrubbedResponse = scrubbedResult?.text ?? "";
|
|
211
|
+
const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
|
|
212
|
+
const usage = response?.usage;
|
|
213
|
+
enqueue({
|
|
214
|
+
model: response?.model ?? model,
|
|
215
|
+
messages: scrubbedMessages,
|
|
216
|
+
response: scrubbedResponse,
|
|
217
|
+
prompt_tokens: usage?.prompt_tokens ?? 0,
|
|
218
|
+
completion_tokens: usage?.completion_tokens ?? 0,
|
|
219
|
+
latency_ms: latencyMs,
|
|
220
|
+
timestamp: requestAt,
|
|
221
|
+
finish_reason: choice?.finish_reason ?? null,
|
|
222
|
+
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
completions[PATCHED] = true;
|
|
230
|
+
}
|
|
231
|
+
function patchAnthropic(client, enqueue) {
|
|
232
|
+
const messagesApi = client.messages;
|
|
233
|
+
if (messagesApi[PATCHED]) return;
|
|
234
|
+
const original = messagesApi.create.bind(messagesApi);
|
|
235
|
+
messagesApi.create = (opts, ...rest) => {
|
|
236
|
+
const requestAt = utcnow();
|
|
237
|
+
const t0 = performance.now();
|
|
238
|
+
const result = original(opts, ...rest);
|
|
239
|
+
if (isStream(opts)) return result;
|
|
240
|
+
return observe(result, (response) => {
|
|
241
|
+
const latencyMs = Math.round(performance.now() - t0);
|
|
242
|
+
setImmediate(() => {
|
|
243
|
+
try {
|
|
244
|
+
const messages = opts?.messages ?? [];
|
|
245
|
+
const system = opts?.system;
|
|
246
|
+
const model = opts?.model ?? "";
|
|
247
|
+
const allMessages = system ? [{ role: "system", content: system }, ...messages] : messages;
|
|
248
|
+
const context = new ScrubContext();
|
|
249
|
+
const [scrubbedMessages, promptDetections] = scrubMessages(allMessages, context);
|
|
250
|
+
const responseText = (response?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
251
|
+
const scrubbedResult = responseText ? scrub(responseText, context) : null;
|
|
252
|
+
const scrubbedResponse = scrubbedResult?.text ?? "";
|
|
253
|
+
const detections = [...promptDetections, ...scrubbedResult?.detections ?? []];
|
|
254
|
+
const usage = response?.usage;
|
|
255
|
+
enqueue({
|
|
256
|
+
model: response?.model ?? model,
|
|
257
|
+
messages: scrubbedMessages,
|
|
258
|
+
response: scrubbedResponse,
|
|
259
|
+
prompt_tokens: usage?.input_tokens ?? 0,
|
|
260
|
+
completion_tokens: usage?.output_tokens ?? 0,
|
|
261
|
+
latency_ms: latencyMs,
|
|
262
|
+
timestamp: requestAt,
|
|
263
|
+
finish_reason: response?.stop_reason ?? null,
|
|
264
|
+
pii_detections: detections.map((d) => ({ category: d.category, placeholder: d.placeholder }))
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
messagesApi[PATCHED] = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/client.ts
|
|
275
|
+
var state = {
|
|
276
|
+
apiKey: null,
|
|
277
|
+
baseUrl: "https://api.sovergate.com",
|
|
278
|
+
queue: null
|
|
279
|
+
};
|
|
280
|
+
function init({ apiKey, baseUrl = "https://api.sovergate.com", debug = false }) {
|
|
281
|
+
try {
|
|
282
|
+
state.apiKey = apiKey;
|
|
283
|
+
state.baseUrl = baseUrl.replace(/\/$/, "");
|
|
284
|
+
if (debug) {
|
|
285
|
+
console.debug("[sovergate] init baseUrl=%s", state.baseUrl);
|
|
286
|
+
}
|
|
287
|
+
state.queue = new LogQueue((payload) => send(apiKey, payload, state.baseUrl));
|
|
288
|
+
} catch (err) {
|
|
289
|
+
state.queue = null;
|
|
290
|
+
warn("init failed, logging disabled", err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function instrument(client) {
|
|
294
|
+
if (!state.queue) {
|
|
295
|
+
warn("instrument() called before init({ apiKey }) \u2014 LLM calls will not be logged");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const enqueue = state.queue.enqueue.bind(state.queue);
|
|
300
|
+
if (client?.chat?.completions) {
|
|
301
|
+
patchOpenAI(client, enqueue);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (client?.messages) {
|
|
305
|
+
patchAnthropic(client, enqueue);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
warn(
|
|
309
|
+
"unsupported client \u2014 LLM calls will not be logged. Pass an OpenAI or Anthropic client instance, or open an issue at https://github.com/sovergate/sdk-js/issues"
|
|
310
|
+
);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
warn("instrument() failed, LLM calls will not be logged", err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function warn(message, err) {
|
|
316
|
+
try {
|
|
317
|
+
console.warn(`[sovergate] ${message}`, ...err === void 0 ? [] : [err]);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function shutdown() {
|
|
322
|
+
state.queue = null;
|
|
323
|
+
}
|
|
324
|
+
export {
|
|
325
|
+
init,
|
|
326
|
+
instrument,
|
|
327
|
+
shutdown
|
|
328
|
+
};
|
|
329
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sovergate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "EU AI Act compliance SDK — automatic, non-invasive audit logging for LLM calls",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "jest --testEnvironment node",
|
|
29
|
+
"test:watch": "jest --watch",
|
|
30
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"eu-ai-act",
|
|
34
|
+
"compliance",
|
|
35
|
+
"gdpr",
|
|
36
|
+
"llm",
|
|
37
|
+
"audit",
|
|
38
|
+
"openai",
|
|
39
|
+
"anthropic",
|
|
40
|
+
"pii",
|
|
41
|
+
"observability"
|
|
42
|
+
],
|
|
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
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@anthropic-ai/sdk": "^1.3.0",
|
|
58
|
+
"@types/jest": "^29.5.0",
|
|
59
|
+
"@types/node": "^20.0.0",
|
|
60
|
+
"jest": "^29.5.0",
|
|
61
|
+
"openai": "^7.43.0",
|
|
62
|
+
"ts-jest": "^29.1.0",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
64
|
+
"typescript": "^5.0.0"
|
|
65
|
+
},
|
|
66
|
+
"jest": {
|
|
67
|
+
"preset": "ts-jest",
|
|
68
|
+
"testEnvironment": "node",
|
|
69
|
+
"testMatch": [
|
|
70
|
+
"**/tests/**/*.test.ts"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|