rehydra 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -873
- package/dist/core/anonymizer.d.ts +9 -1
- package/dist/core/anonymizer.d.ts.map +1 -1
- package/dist/core/anonymizer.js +29 -7
- package/dist/core/anonymizer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +11 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/providers/anthropic.d.ts +17 -0
- package/dist/proxy/providers/anthropic.d.ts.map +1 -0
- package/dist/proxy/providers/anthropic.js +117 -0
- package/dist/proxy/providers/anthropic.js.map +1 -0
- package/dist/proxy/providers/index.d.ts +19 -0
- package/dist/proxy/providers/index.d.ts.map +1 -0
- package/dist/proxy/providers/index.js +40 -0
- package/dist/proxy/providers/index.js.map +1 -0
- package/dist/proxy/providers/openai.d.ts +17 -0
- package/dist/proxy/providers/openai.d.ts.map +1 -0
- package/dist/proxy/providers/openai.js +92 -0
- package/dist/proxy/providers/openai.js.map +1 -0
- package/dist/proxy/providers/types.d.ts +29 -0
- package/dist/proxy/providers/types.d.ts.map +1 -0
- package/dist/proxy/providers/types.js +6 -0
- package/dist/proxy/providers/types.js.map +1 -0
- package/dist/proxy/proxy-server.d.ts +53 -0
- package/dist/proxy/proxy-server.d.ts.map +1 -0
- package/dist/proxy/proxy-server.js +146 -0
- package/dist/proxy/proxy-server.js.map +1 -0
- package/dist/proxy/rehydra-fetch.d.ts +35 -0
- package/dist/proxy/rehydra-fetch.d.ts.map +1 -0
- package/dist/proxy/rehydra-fetch.js +217 -0
- package/dist/proxy/rehydra-fetch.js.map +1 -0
- package/dist/proxy/rehydra-proxy.d.ts +40 -0
- package/dist/proxy/rehydra-proxy.d.ts.map +1 -0
- package/dist/proxy/rehydra-proxy.js +82 -0
- package/dist/proxy/rehydra-proxy.js.map +1 -0
- package/dist/proxy/sse-parser.d.ts +59 -0
- package/dist/proxy/sse-parser.d.ts.map +1 -0
- package/dist/proxy/sse-parser.js +112 -0
- package/dist/proxy/sse-parser.js.map +1 -0
- package/dist/proxy/types.d.ts +49 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +5 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/proxy/wrap-client.d.ts +47 -0
- package/dist/proxy/wrap-client.d.ts.map +1 -0
- package/dist/proxy/wrap-client.js +70 -0
- package/dist/proxy/wrap-client.js.map +1 -0
- package/dist/storage/session.d.ts +3 -0
- package/dist/storage/session.d.ts.map +1 -1
- package/dist/storage/session.js +24 -1
- package/dist/storage/session.js.map +1 -1
- package/dist/storage/types.d.ts +16 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/streaming/anonymizer-stream.d.ts +63 -0
- package/dist/streaming/anonymizer-stream.d.ts.map +1 -0
- package/dist/streaming/anonymizer-stream.js +184 -0
- package/dist/streaming/anonymizer-stream.js.map +1 -0
- package/dist/streaming/index.d.ts +9 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +8 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/sentence-buffer.d.ts +78 -0
- package/dist/streaming/sentence-buffer.d.ts.map +1 -0
- package/dist/streaming/sentence-buffer.js +238 -0
- package/dist/streaming/sentence-buffer.js.map +1 -0
- package/dist/streaming/stream-factory.d.ts +38 -0
- package/dist/streaming/stream-factory.d.ts.map +1 -0
- package/dist/streaming/stream-factory.js +69 -0
- package/dist/streaming/stream-factory.js.map +1 -0
- package/dist/streaming/types.d.ts +121 -0
- package/dist/streaming/types.d.ts.map +1 -0
- package/dist/streaming/types.js +5 -0
- package/dist/streaming/types.js.map +1 -0
- package/dist/types/index.d.ts +8 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +19 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/proxy/providers/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Proxy Server
|
|
3
|
+
* Standalone HTTP server that proxies LLM API requests with
|
|
4
|
+
* automatic PII anonymization and rehydration.
|
|
5
|
+
*/
|
|
6
|
+
import { type Server } from "node:http";
|
|
7
|
+
import type { RehydraProxyConfig } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for the standalone proxy server
|
|
10
|
+
*/
|
|
11
|
+
export interface RehydraProxyServerConfig extends RehydraProxyConfig {
|
|
12
|
+
/** Port to listen on */
|
|
13
|
+
port: number;
|
|
14
|
+
/** Host to bind to (default: "127.0.0.1") */
|
|
15
|
+
host?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A running Rehydra proxy server
|
|
19
|
+
*/
|
|
20
|
+
export interface RehydraProxyServer {
|
|
21
|
+
/** The underlying HTTP server */
|
|
22
|
+
server: Server;
|
|
23
|
+
/** The port the server is listening on */
|
|
24
|
+
port: number;
|
|
25
|
+
/** The host the server is bound to */
|
|
26
|
+
host: string;
|
|
27
|
+
/** Gracefully close the server */
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates and starts a standalone HTTP proxy server that anonymizes
|
|
32
|
+
* LLM API requests and rehydrates responses.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const proxy = await createRehydraProxyServer({
|
|
37
|
+
* port: 8080,
|
|
38
|
+
* upstream: 'https://api.openai.com',
|
|
39
|
+
* keyProvider: new ConfigKeyProvider(process.env.PII_KEY!),
|
|
40
|
+
* piiStorageProvider: new SQLitePIIStorageProvider('proxy.db'),
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* console.log(`Proxy running on http://${proxy.host}:${proxy.port}`);
|
|
44
|
+
*
|
|
45
|
+
* // Point your OpenAI client at the proxy:
|
|
46
|
+
* const openai = new OpenAI({ baseURL: `http://localhost:${proxy.port}/v1` });
|
|
47
|
+
*
|
|
48
|
+
* // Stop the server
|
|
49
|
+
* await proxy.close();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function createRehydraProxyServer(config: RehydraProxyServerConfig): Promise<RehydraProxyServer>;
|
|
53
|
+
//# sourceMappingURL=proxy-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-server.d.ts","sourceRoot":"","sources":["../../src/proxy/proxy-server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AAEjG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,kBAAkB;IAClE,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAwC7B"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Proxy Server
|
|
3
|
+
* Standalone HTTP server that proxies LLM API requests with
|
|
4
|
+
* automatic PII anonymization and rehydration.
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { createRehydraProxy } from "./rehydra-proxy.js";
|
|
8
|
+
/**
|
|
9
|
+
* Creates and starts a standalone HTTP proxy server that anonymizes
|
|
10
|
+
* LLM API requests and rehydrates responses.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const proxy = await createRehydraProxyServer({
|
|
15
|
+
* port: 8080,
|
|
16
|
+
* upstream: 'https://api.openai.com',
|
|
17
|
+
* keyProvider: new ConfigKeyProvider(process.env.PII_KEY!),
|
|
18
|
+
* piiStorageProvider: new SQLitePIIStorageProvider('proxy.db'),
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* console.log(`Proxy running on http://${proxy.host}:${proxy.port}`);
|
|
22
|
+
*
|
|
23
|
+
* // Point your OpenAI client at the proxy:
|
|
24
|
+
* const openai = new OpenAI({ baseURL: `http://localhost:${proxy.port}/v1` });
|
|
25
|
+
*
|
|
26
|
+
* // Stop the server
|
|
27
|
+
* await proxy.close();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export async function createRehydraProxyServer(config) {
|
|
31
|
+
const host = config.host ?? "127.0.0.1";
|
|
32
|
+
const proxy = createRehydraProxy(config);
|
|
33
|
+
const server = createServer((req, res) => {
|
|
34
|
+
void (async () => {
|
|
35
|
+
try {
|
|
36
|
+
const webRequest = incomingMessageToRequest(req, host, config.port);
|
|
37
|
+
const webResponse = await proxy(webRequest);
|
|
38
|
+
await writeResponse(res, webResponse);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
42
|
+
res.end(JSON.stringify({
|
|
43
|
+
error: "proxy_error",
|
|
44
|
+
message: error instanceof Error ? error.message : "Unknown proxy error",
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
});
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
server.on("error", reject);
|
|
51
|
+
server.listen(config.port, host, () => {
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
server,
|
|
57
|
+
port: config.port,
|
|
58
|
+
host,
|
|
59
|
+
async close() {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
server.close((err) => {
|
|
62
|
+
if (err !== undefined)
|
|
63
|
+
reject(err);
|
|
64
|
+
else
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert a Node.js IncomingMessage to a Web API Request
|
|
73
|
+
*/
|
|
74
|
+
function incomingMessageToRequest(req, host, port) {
|
|
75
|
+
const url = `http://${host}:${port}${req.url ?? "/"}`;
|
|
76
|
+
const headers = new Headers();
|
|
77
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
78
|
+
if (value !== undefined) {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const v of value) {
|
|
81
|
+
headers.append(key, v);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
headers.set(key, value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const method = req.method ?? "GET";
|
|
90
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
91
|
+
return new Request(url, {
|
|
92
|
+
method,
|
|
93
|
+
headers,
|
|
94
|
+
body: hasBody ? nodeStreamToReadableStream(req) : undefined,
|
|
95
|
+
// @ts-expect-error - duplex is needed for streaming request bodies
|
|
96
|
+
duplex: hasBody ? "half" : undefined,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convert a Node.js Readable stream to a Web API ReadableStream
|
|
101
|
+
*/
|
|
102
|
+
function nodeStreamToReadableStream(nodeStream) {
|
|
103
|
+
return new ReadableStream({
|
|
104
|
+
start(controller) {
|
|
105
|
+
nodeStream.on("data", (chunk) => {
|
|
106
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
107
|
+
});
|
|
108
|
+
nodeStream.on("end", () => {
|
|
109
|
+
controller.close();
|
|
110
|
+
});
|
|
111
|
+
nodeStream.on("error", (err) => {
|
|
112
|
+
controller.error(err);
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Write a Web API Response to a Node.js ServerResponse
|
|
119
|
+
*/
|
|
120
|
+
async function writeResponse(res, webResponse) {
|
|
121
|
+
// Copy status and headers
|
|
122
|
+
const headers = {};
|
|
123
|
+
webResponse.headers.forEach((value, key) => {
|
|
124
|
+
headers[key] = value;
|
|
125
|
+
});
|
|
126
|
+
res.writeHead(webResponse.status, headers);
|
|
127
|
+
if (webResponse.body === null) {
|
|
128
|
+
res.end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Stream the response body
|
|
132
|
+
const reader = webResponse.body.getReader();
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const { done, value } = await reader.read();
|
|
136
|
+
if (done)
|
|
137
|
+
break;
|
|
138
|
+
res.write(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
reader.releaseLock();
|
|
143
|
+
res.end();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=proxy-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-server.js","sourceRoot":"","sources":["../../src/proxy/proxy-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Fetch Wrapper
|
|
3
|
+
* Wraps the native fetch to anonymize outgoing LLM requests
|
|
4
|
+
* and rehydrate incoming LLM responses.
|
|
5
|
+
*/
|
|
6
|
+
import type { RehydraFetchConfig } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a fetch-compatible function that automatically:
|
|
9
|
+
* 1. Anonymizes text in outgoing LLM API requests
|
|
10
|
+
* 2. Rehydrates text in incoming LLM API responses
|
|
11
|
+
*
|
|
12
|
+
* PII never leaves your infrastructure.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import OpenAI from 'openai';
|
|
17
|
+
* import { createRehydraFetch, InMemoryKeyProvider, InMemoryPIIStorageProvider } from 'rehydra';
|
|
18
|
+
*
|
|
19
|
+
* const rehydraFetch = createRehydraFetch({
|
|
20
|
+
* keyProvider: new InMemoryKeyProvider(),
|
|
21
|
+
* piiStorageProvider: new InMemoryPIIStorageProvider(),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const openai = new OpenAI({ fetch: rehydraFetch });
|
|
25
|
+
*
|
|
26
|
+
* // PII is automatically anonymized before being sent to OpenAI
|
|
27
|
+
* const response = await openai.chat.completions.create({
|
|
28
|
+
* model: 'gpt-4',
|
|
29
|
+
* messages: [{ role: 'user', content: 'Email john@example.com about the meeting' }],
|
|
30
|
+
* });
|
|
31
|
+
* // Response is automatically rehydrated — contains original PII
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRehydraFetch(config: RehydraFetchConfig): typeof globalThis.fetch;
|
|
35
|
+
//# sourceMappingURL=rehydra-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehydra-fetch.d.ts","sourceRoot":"","sources":["../../src/proxy/rehydra-fetch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,UAAU,CAAC,KAAK,CA0FzB"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Fetch Wrapper
|
|
3
|
+
* Wraps the native fetch to anonymize outgoing LLM requests
|
|
4
|
+
* and rehydrate incoming LLM responses.
|
|
5
|
+
*/
|
|
6
|
+
import { createAnonymizer } from "../core/anonymizer.js";
|
|
7
|
+
import { decryptPIIMap } from "../crypto/index.js";
|
|
8
|
+
import { rehydrate } from "../pipeline/tagger.js";
|
|
9
|
+
import { AnonymizerSessionImpl } from "../storage/session.js";
|
|
10
|
+
import { SSEParser, isSSEDone, serializeSSEEvent } from "./sse-parser.js";
|
|
11
|
+
import { detectProvider } from "./providers/index.js";
|
|
12
|
+
let sessionCounter = 0;
|
|
13
|
+
function defaultGetSessionId() {
|
|
14
|
+
return `rehydra-proxy-${Date.now()}-${++sessionCounter}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a fetch-compatible function that automatically:
|
|
18
|
+
* 1. Anonymizes text in outgoing LLM API requests
|
|
19
|
+
* 2. Rehydrates text in incoming LLM API responses
|
|
20
|
+
*
|
|
21
|
+
* PII never leaves your infrastructure.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import OpenAI from 'openai';
|
|
26
|
+
* import { createRehydraFetch, InMemoryKeyProvider, InMemoryPIIStorageProvider } from 'rehydra';
|
|
27
|
+
*
|
|
28
|
+
* const rehydraFetch = createRehydraFetch({
|
|
29
|
+
* keyProvider: new InMemoryKeyProvider(),
|
|
30
|
+
* piiStorageProvider: new InMemoryPIIStorageProvider(),
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* const openai = new OpenAI({ fetch: rehydraFetch });
|
|
34
|
+
*
|
|
35
|
+
* // PII is automatically anonymized before being sent to OpenAI
|
|
36
|
+
* const response = await openai.chat.completions.create({
|
|
37
|
+
* model: 'gpt-4',
|
|
38
|
+
* messages: [{ role: 'user', content: 'Email john@example.com about the meeting' }],
|
|
39
|
+
* });
|
|
40
|
+
* // Response is automatically rehydrated — contains original PII
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function createRehydraFetch(config) {
|
|
44
|
+
const anonymizer = createAnonymizer({
|
|
45
|
+
...config.anonymizer,
|
|
46
|
+
keyProvider: config.keyProvider,
|
|
47
|
+
piiStorageProvider: config.piiStorageProvider,
|
|
48
|
+
});
|
|
49
|
+
let initialized = false;
|
|
50
|
+
const getSessionId = config.getSessionId ?? defaultGetSessionId;
|
|
51
|
+
const handleStreaming = config.handleStreaming !== false;
|
|
52
|
+
async function ensureInitialized() {
|
|
53
|
+
if (!initialized) {
|
|
54
|
+
await anonymizer.initialize();
|
|
55
|
+
initialized = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return async (input, init) => {
|
|
59
|
+
await ensureInitialized();
|
|
60
|
+
const request = new Request(input, init);
|
|
61
|
+
// Only intercept POST requests with JSON bodies (LLM API calls)
|
|
62
|
+
if (request.method !== "POST") {
|
|
63
|
+
return fetch(request);
|
|
64
|
+
}
|
|
65
|
+
const contentType = request.headers.get("content-type");
|
|
66
|
+
if (contentType === null || !contentType.includes("application/json")) {
|
|
67
|
+
return fetch(request);
|
|
68
|
+
}
|
|
69
|
+
// Detect the LLM provider
|
|
70
|
+
const provider = detectProvider(request.url, request.headers, config.provider);
|
|
71
|
+
// Parse request body
|
|
72
|
+
const body = await request.json();
|
|
73
|
+
// Get session ID for PII map persistence
|
|
74
|
+
const sessionId = await getSessionId(request);
|
|
75
|
+
// Create a session for this request
|
|
76
|
+
const session = new AnonymizerSessionImpl(anonymizer, sessionId, config.piiStorageProvider, config.keyProvider);
|
|
77
|
+
// Extract and anonymize text from the request
|
|
78
|
+
const texts = provider.extractRequestText(body);
|
|
79
|
+
const anonymizedTexts = [];
|
|
80
|
+
for (const text of texts) {
|
|
81
|
+
const result = await session.anonymize(text, config.locale, config.policy);
|
|
82
|
+
anonymizedTexts.push(result.anonymizedText);
|
|
83
|
+
}
|
|
84
|
+
// Rebuild the request body with anonymized text
|
|
85
|
+
const anonymizedBody = provider.rebuildRequestBody(body, anonymizedTexts);
|
|
86
|
+
// Forward the anonymized request
|
|
87
|
+
const upstreamRequest = new Request(request, {
|
|
88
|
+
body: JSON.stringify(anonymizedBody),
|
|
89
|
+
});
|
|
90
|
+
const response = await fetch(upstreamRequest);
|
|
91
|
+
// Determine if the response is a streaming SSE response
|
|
92
|
+
const responseContentType = response.headers.get("content-type");
|
|
93
|
+
const isSSE = handleStreaming &&
|
|
94
|
+
provider.isStreamingRequest(body) &&
|
|
95
|
+
responseContentType !== null &&
|
|
96
|
+
responseContentType.includes("text/event-stream");
|
|
97
|
+
if (isSSE && response.body !== null) {
|
|
98
|
+
return rehydrateSSEResponse(response, session, provider, config);
|
|
99
|
+
}
|
|
100
|
+
return rehydrateJSONResponse(response, session, provider);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Rehydrate a non-streaming JSON response.
|
|
105
|
+
*/
|
|
106
|
+
async function rehydrateJSONResponse(response, session, provider) {
|
|
107
|
+
const body = await response.json();
|
|
108
|
+
// Extract response text and rehydrate
|
|
109
|
+
const responseTexts = provider.extractResponseText(body);
|
|
110
|
+
const rehydratedTexts = [];
|
|
111
|
+
for (const text of responseTexts) {
|
|
112
|
+
const rehydrated = await session.rehydrate(text);
|
|
113
|
+
rehydratedTexts.push(rehydrated);
|
|
114
|
+
}
|
|
115
|
+
// Rebuild response body
|
|
116
|
+
const rehydratedBody = provider.rebuildResponseBody(body, rehydratedTexts);
|
|
117
|
+
return new Response(JSON.stringify(rehydratedBody), {
|
|
118
|
+
status: response.status,
|
|
119
|
+
statusText: response.statusText,
|
|
120
|
+
headers: response.headers,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Rehydrate a streaming SSE response.
|
|
125
|
+
* Returns a new Response with a transformed body stream.
|
|
126
|
+
*/
|
|
127
|
+
function rehydrateSSEResponse(response, session, provider, config) {
|
|
128
|
+
const sseParser = new SSEParser();
|
|
129
|
+
const decoder = new TextDecoder();
|
|
130
|
+
const encoder = new TextEncoder();
|
|
131
|
+
// Buffer for incomplete PII tags that span SSE chunks
|
|
132
|
+
let tagBuffer = "";
|
|
133
|
+
// We load the PII map once, lazily
|
|
134
|
+
let piiMapPromise = null;
|
|
135
|
+
function getPiiMap() {
|
|
136
|
+
if (piiMapPromise === null) {
|
|
137
|
+
piiMapPromise = (async () => {
|
|
138
|
+
const stored = await config.piiStorageProvider.load(session.sessionId);
|
|
139
|
+
if (stored === null)
|
|
140
|
+
return new Map();
|
|
141
|
+
const key = await config.keyProvider.getKey();
|
|
142
|
+
return decryptPIIMap(stored.piiMap, key);
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
145
|
+
return piiMapPromise;
|
|
146
|
+
}
|
|
147
|
+
const transformStream = new TransformStream({
|
|
148
|
+
async transform(chunk, controller) {
|
|
149
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
150
|
+
const events = sseParser.parse(text);
|
|
151
|
+
for (const event of events) {
|
|
152
|
+
if (isSSEDone(event.data)) {
|
|
153
|
+
controller.enqueue(encoder.encode(serializeSSEEvent(event)));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(event.data);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Not JSON — pass through as-is
|
|
162
|
+
controller.enqueue(encoder.encode(serializeSSEEvent(event)));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const delta = provider.extractSSEDelta(parsed);
|
|
166
|
+
if (delta === null) {
|
|
167
|
+
controller.enqueue(encoder.encode(serializeSSEEvent(event)));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// Handle incomplete PII tags spanning chunks
|
|
171
|
+
const fullText = tagBuffer + delta;
|
|
172
|
+
const incompleteTagIdx = fullText.lastIndexOf("<PII");
|
|
173
|
+
const lastCloseIdx = fullText.lastIndexOf("/>");
|
|
174
|
+
let textToRehydrate;
|
|
175
|
+
if (incompleteTagIdx !== -1 &&
|
|
176
|
+
(lastCloseIdx === -1 || lastCloseIdx < incompleteTagIdx)) {
|
|
177
|
+
textToRehydrate = fullText.slice(0, incompleteTagIdx);
|
|
178
|
+
tagBuffer = fullText.slice(incompleteTagIdx);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
textToRehydrate = fullText;
|
|
182
|
+
tagBuffer = "";
|
|
183
|
+
}
|
|
184
|
+
if (textToRehydrate.length > 0) {
|
|
185
|
+
const piiMap = await getPiiMap();
|
|
186
|
+
const rehydrated = rehydrate(textToRehydrate, piiMap);
|
|
187
|
+
const rebuilt = provider.rebuildSSEDelta(parsed, rehydrated);
|
|
188
|
+
controller.enqueue(encoder.encode(serializeSSEEvent({
|
|
189
|
+
event: event.event,
|
|
190
|
+
data: JSON.stringify(rebuilt),
|
|
191
|
+
})));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async flush(controller) {
|
|
196
|
+
const events = sseParser.flush();
|
|
197
|
+
for (const event of events) {
|
|
198
|
+
controller.enqueue(encoder.encode(serializeSSEEvent(event)));
|
|
199
|
+
}
|
|
200
|
+
if (tagBuffer.length > 0) {
|
|
201
|
+
const piiMap = await getPiiMap();
|
|
202
|
+
const rehydrated = rehydrate(tagBuffer, piiMap);
|
|
203
|
+
if (rehydrated.length > 0) {
|
|
204
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: rehydrated })}\n\n`));
|
|
205
|
+
}
|
|
206
|
+
tagBuffer = "";
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const transformedBody = response.body.pipeThrough(transformStream);
|
|
211
|
+
return new Response(transformedBody, {
|
|
212
|
+
status: response.status,
|
|
213
|
+
statusText: response.statusText,
|
|
214
|
+
headers: response.headers,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=rehydra-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehydra-fetch.js","sourceRoot":"","sources":["../../src/proxy/rehydra-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Proxy Middleware
|
|
3
|
+
* Generic (Request → Response) proxy that anonymizes LLM requests
|
|
4
|
+
* and rehydrates responses. Uses standard Web APIs — no framework dependency.
|
|
5
|
+
*/
|
|
6
|
+
import type { RehydraProxyConfig } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a proxy middleware function that forwards requests to an upstream
|
|
9
|
+
* LLM API, anonymizing requests and rehydrating responses.
|
|
10
|
+
*
|
|
11
|
+
* Returns a standard `(Request) => Promise<Response>` function that works
|
|
12
|
+
* with any framework supporting Web Request/Response APIs.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Hono
|
|
17
|
+
* import { Hono } from 'hono';
|
|
18
|
+
* const app = new Hono();
|
|
19
|
+
* const proxy = createRehydraProxy({
|
|
20
|
+
* upstream: 'https://api.openai.com',
|
|
21
|
+
* keyProvider: new ConfigKeyProvider(process.env.PII_KEY!),
|
|
22
|
+
* piiStorageProvider: new SQLitePIIStorageProvider('proxy.db'),
|
|
23
|
+
* });
|
|
24
|
+
* app.post('/v1/*', (c) => proxy(c.req.raw));
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Bun.serve
|
|
30
|
+
* const proxy = createRehydraProxy({ upstream: 'https://api.openai.com', ... });
|
|
31
|
+
* Bun.serve({
|
|
32
|
+
* fetch(req) {
|
|
33
|
+
* if (new URL(req.url).pathname.startsWith('/v1/')) return proxy(req);
|
|
34
|
+
* return new Response('Not Found', { status: 404 });
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRehydraProxy(config: RehydraProxyConfig): (request: Request) => Promise<Response>;
|
|
40
|
+
//# sourceMappingURL=rehydra-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehydra-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/rehydra-proxy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,kBAAkB,GACzB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwCzC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehydra Proxy Middleware
|
|
3
|
+
* Generic (Request → Response) proxy that anonymizes LLM requests
|
|
4
|
+
* and rehydrates responses. Uses standard Web APIs — no framework dependency.
|
|
5
|
+
*/
|
|
6
|
+
import { createRehydraFetch } from "./rehydra-fetch.js";
|
|
7
|
+
const DEFAULT_FORWARD_HEADERS = [
|
|
8
|
+
"authorization",
|
|
9
|
+
"content-type",
|
|
10
|
+
"x-api-key",
|
|
11
|
+
"anthropic-version",
|
|
12
|
+
"openai-organization",
|
|
13
|
+
"openai-project",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Creates a proxy middleware function that forwards requests to an upstream
|
|
17
|
+
* LLM API, anonymizing requests and rehydrating responses.
|
|
18
|
+
*
|
|
19
|
+
* Returns a standard `(Request) => Promise<Response>` function that works
|
|
20
|
+
* with any framework supporting Web Request/Response APIs.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Hono
|
|
25
|
+
* import { Hono } from 'hono';
|
|
26
|
+
* const app = new Hono();
|
|
27
|
+
* const proxy = createRehydraProxy({
|
|
28
|
+
* upstream: 'https://api.openai.com',
|
|
29
|
+
* keyProvider: new ConfigKeyProvider(process.env.PII_KEY!),
|
|
30
|
+
* piiStorageProvider: new SQLitePIIStorageProvider('proxy.db'),
|
|
31
|
+
* });
|
|
32
|
+
* app.post('/v1/*', (c) => proxy(c.req.raw));
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Bun.serve
|
|
38
|
+
* const proxy = createRehydraProxy({ upstream: 'https://api.openai.com', ... });
|
|
39
|
+
* Bun.serve({
|
|
40
|
+
* fetch(req) {
|
|
41
|
+
* if (new URL(req.url).pathname.startsWith('/v1/')) return proxy(req);
|
|
42
|
+
* return new Response('Not Found', { status: 404 });
|
|
43
|
+
* },
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createRehydraProxy(config) {
|
|
48
|
+
const forwardHeaders = config.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
|
|
49
|
+
const upstream = config.upstream.replace(/\/$/, ""); // Remove trailing slash
|
|
50
|
+
// Create the underlying Rehydra fetch wrapper
|
|
51
|
+
const rehydraFetch = createRehydraFetch(config);
|
|
52
|
+
return async (request) => {
|
|
53
|
+
// Build upstream URL
|
|
54
|
+
const requestUrl = new URL(request.url);
|
|
55
|
+
let pathname = requestUrl.pathname;
|
|
56
|
+
// Strip prefix if configured
|
|
57
|
+
if (config.stripPrefix !== undefined && config.stripPrefix !== "" && pathname.startsWith(config.stripPrefix)) {
|
|
58
|
+
pathname = pathname.slice(config.stripPrefix.length);
|
|
59
|
+
if (!pathname.startsWith("/")) {
|
|
60
|
+
pathname = "/" + pathname;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const upstreamUrl = upstream + pathname + requestUrl.search;
|
|
64
|
+
// Filter headers to forward
|
|
65
|
+
const headers = new Headers();
|
|
66
|
+
for (const name of forwardHeaders) {
|
|
67
|
+
const value = request.headers.get(name);
|
|
68
|
+
if (value !== null) {
|
|
69
|
+
headers.set(name, value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Forward via rehydraFetch (which handles anonymization/rehydration)
|
|
73
|
+
return rehydraFetch(upstreamUrl, {
|
|
74
|
+
method: request.method,
|
|
75
|
+
headers,
|
|
76
|
+
body: request.body,
|
|
77
|
+
// @ts-expect-error - duplex is needed for streaming request bodies
|
|
78
|
+
duplex: "half",
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=rehydra-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehydra-proxy.js","sourceRoot":"","sources":["../../src/proxy/rehydra-proxy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD,MAAM,uBAAuB,GAAG;IAC9B,eAAe;IACf,cAAc;IACd,WAAW;IACX,mBAAmB;IACnB,qBAAqB;IACrB,gBAAgB;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA0B;IAE1B,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,uBAAuB,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,wBAAwB;IAE7E,8CAA8C;IAC9C,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEhD,OAAO,KAAK,EAAE,OAAgB,EAAqB,EAAE;QACnD,qBAAqB;QACrB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;QAEnC,6BAA6B;QAC7B,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,EAAE,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7G,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACrD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,QAAQ,GAAG,GAAG,GAAG,QAAQ,CAAC;YAC5B,CAAC;QACH,CAAC;QAED,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC;QAE5D,4BAA4B;QAC5B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,OAAO,YAAY,CAAC,WAAW,EAAE;YAC/B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO;YACP,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,mEAAmE;YACnE,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|