@privacyscrubber/sdk 2.4.5 → 2.4.7
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 +50 -1
- package/browser.js +431 -26
- package/cli.js +119 -11
- package/examples/python-bridge/README.md +3 -3
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/langchain_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +2 -2
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/fastmcp.cjs +56 -0
- package/fastmcp.d.ts +5 -0
- package/index.cjs +1186 -88
- package/index.d.ts +156 -2
- package/index.js +1186 -88
- package/package.json +13 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +14 -2
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +123 -34
- package/vercel-ai.d.ts +7 -0
- package/benchmark.js +0 -355
- package/examples/python-bridge/bridge_daemon.cjs +0 -260
- package/examples/python-bridge/langchain_adapter.py +0 -20
- package/examples/python-bridge/privacyscrubber.py +0 -36
- package/examples/python-bridge/test_python_bridge.py +0 -320
package/index.js
CHANGED
|
@@ -34,6 +34,16 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
|
34
34
|
scrubberPkg.init();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export const SDK_VERSION = (() => {
|
|
38
|
+
try {
|
|
39
|
+
const pkgJson = require('./package.json');
|
|
40
|
+
if (pkgJson && typeof pkgJson.version === 'string') {
|
|
41
|
+
return pkgJson.version;
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
return '2.4.7';
|
|
45
|
+
})();
|
|
46
|
+
|
|
37
47
|
/**
|
|
38
48
|
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
39
49
|
*/
|
|
@@ -80,7 +90,10 @@ export class PrivacyScrubberLicenseError extends Error {
|
|
|
80
90
|
this.currentLength = currentLength;
|
|
81
91
|
this.limit = limit;
|
|
82
92
|
this.profile = profile;
|
|
83
|
-
|
|
93
|
+
const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
|
|
94
|
+
this.upgradeUrl = `https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
95
|
+
this.trialUrl = `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
96
|
+
this.trialCommand = 'npx @privacyscrubber/sdk trial';
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -187,11 +200,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
187
200
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
188
201
|
};
|
|
189
202
|
|
|
203
|
+
const agenticProtocol = {
|
|
204
|
+
protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
|
|
205
|
+
agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
|
|
206
|
+
ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
|
|
207
|
+
regulatorySafeHarbor: [
|
|
208
|
+
'GDPR-Art-4(5)-Pseudonymization',
|
|
209
|
+
'GDPR-Art-25-Privacy-by-Design',
|
|
210
|
+
'GDPR-Art-32-TOMs',
|
|
211
|
+
'EU-AI-Act-Art-50',
|
|
212
|
+
'EU-AI-Act-Art-17-EN-18286-QMS',
|
|
213
|
+
'CNIL-ICO-Agentic-Memory-Compartmentalization',
|
|
214
|
+
'IL-PPL-Amendment-13-Sec-17'
|
|
215
|
+
],
|
|
216
|
+
zeroCleartextEgress: true
|
|
217
|
+
};
|
|
218
|
+
|
|
190
219
|
const statutoryCitations = [
|
|
191
220
|
{
|
|
192
|
-
statute: 'EU AI Act (Art. 50)',
|
|
193
|
-
scope: 'AI Transparency &
|
|
194
|
-
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
221
|
+
statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
|
|
222
|
+
scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
|
|
223
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
|
|
195
224
|
},
|
|
196
225
|
{
|
|
197
226
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
@@ -223,6 +252,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
223
252
|
frameworksList: Array.from(frameworksSet),
|
|
224
253
|
statutoryCitations,
|
|
225
254
|
ipHoldings,
|
|
255
|
+
agenticProtocol,
|
|
226
256
|
multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
|
|
227
257
|
};
|
|
228
258
|
}
|
|
@@ -531,7 +561,8 @@ export function sanitize(text, options = {}) {
|
|
|
531
561
|
if (!isPro && text.length > limit) {
|
|
532
562
|
if (shouldThrow) {
|
|
533
563
|
const expiredPrefix = isExpiredKey ? `[PILOT/TRIAL EXPIRED: ${licenseCheck.error}] ` : '';
|
|
534
|
-
const
|
|
564
|
+
const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
|
|
565
|
+
const errorMsg = `[PrivacyScrubber] ${expiredPrefix}Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to Developer SDK ($299/mo) for unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&src=${isExpiredKey ? 'pilot_expired' : 'dev_limit'} (Start 7-day B2B trial: 'npx @privacyscrubber/sdk trial' or 'npx @privacyscrubber/sdk memo' for CISO memo)`;
|
|
535
566
|
throw new PrivacyScrubberLicenseError(errorMsg, {
|
|
536
567
|
tier: 'FREE',
|
|
537
568
|
currentLength: text.length,
|
|
@@ -612,12 +643,17 @@ export function sanitize(text, options = {}) {
|
|
|
612
643
|
tier: tier,
|
|
613
644
|
limit: limit,
|
|
614
645
|
wasTruncated: wasTruncated,
|
|
646
|
+
truncated: wasTruncated,
|
|
615
647
|
...(isSeatTier && {
|
|
616
|
-
upgradeUrl:
|
|
648
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
|
|
649
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
|
|
650
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
617
651
|
recommendation: `Your ${rawTier} license is valid for interactive browser seats only. Upgrade to Developer SDK ($299/mo) for unlimited backend server throughput: https://privacyscrubber.com/pricing?tier=sdk`
|
|
618
652
|
}),
|
|
619
653
|
...(!isPro && !isSeatTier && {
|
|
620
|
-
upgradeUrl:
|
|
654
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
|
|
655
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
|
|
656
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
621
657
|
recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited backend text processing, custom regex rules, and specialized compliance profiles.'
|
|
622
658
|
})
|
|
623
659
|
},
|
|
@@ -724,6 +760,21 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
724
760
|
|
|
725
761
|
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
726
762
|
let totalCount = 0;
|
|
763
|
+
const seen = new WeakMap();
|
|
764
|
+
|
|
765
|
+
function isNonPlainObject(val) {
|
|
766
|
+
return (
|
|
767
|
+
val instanceof Date ||
|
|
768
|
+
val instanceof RegExp ||
|
|
769
|
+
val instanceof Error ||
|
|
770
|
+
val instanceof Promise ||
|
|
771
|
+
val instanceof WeakMap ||
|
|
772
|
+
val instanceof WeakSet ||
|
|
773
|
+
(typeof URL !== 'undefined' && val instanceof URL) ||
|
|
774
|
+
ArrayBuffer.isView(val) ||
|
|
775
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
776
|
+
);
|
|
777
|
+
}
|
|
727
778
|
|
|
728
779
|
function traverseAndSanitize(val) {
|
|
729
780
|
if (val === null || val === undefined) return val;
|
|
@@ -738,20 +789,42 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
738
789
|
totalCount += res.count;
|
|
739
790
|
return res.scrubbedText;
|
|
740
791
|
}
|
|
792
|
+
if (typeof val !== 'object') return val;
|
|
793
|
+
if (isNonPlainObject(val)) return val;
|
|
794
|
+
if (seen.has(val)) return seen.get(val);
|
|
795
|
+
|
|
741
796
|
if (Array.isArray(val)) {
|
|
742
|
-
|
|
797
|
+
const outArr = [];
|
|
798
|
+
seen.set(val, outArr);
|
|
799
|
+
for (let i = 0; i < val.length; i++) {
|
|
800
|
+
outArr.push(traverseAndSanitize(val[i]));
|
|
801
|
+
}
|
|
802
|
+
return outArr;
|
|
743
803
|
}
|
|
744
|
-
if (val instanceof
|
|
745
|
-
|
|
804
|
+
if (val instanceof Map) {
|
|
805
|
+
const outMap = new Map();
|
|
806
|
+
seen.set(val, outMap);
|
|
807
|
+
for (const [k, v] of val.entries()) {
|
|
808
|
+
outMap.set(k, traverseAndSanitize(v));
|
|
809
|
+
}
|
|
810
|
+
return outMap;
|
|
746
811
|
}
|
|
747
|
-
if (
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
812
|
+
if (val instanceof Set) {
|
|
813
|
+
const outSet = new Set();
|
|
814
|
+
seen.set(val, outSet);
|
|
815
|
+
for (const item of val.values()) {
|
|
816
|
+
outSet.add(traverseAndSanitize(item));
|
|
751
817
|
}
|
|
752
|
-
return
|
|
818
|
+
return outSet;
|
|
753
819
|
}
|
|
754
|
-
|
|
820
|
+
|
|
821
|
+
const out = {};
|
|
822
|
+
seen.set(val, out);
|
|
823
|
+
for (const [k, v] of Object.entries(val)) {
|
|
824
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
825
|
+
out[k] = traverseAndSanitize(v);
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
755
828
|
}
|
|
756
829
|
|
|
757
830
|
const sanitizedResult = traverseAndSanitize(obj);
|
|
@@ -781,23 +854,62 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
781
854
|
* @param {Record<string, string>} tokenMap - Token map returned from sanitize() or sanitizeObject().
|
|
782
855
|
* @returns {any} - Restored object with original values.
|
|
783
856
|
*/
|
|
784
|
-
export function restoreObject(obj, tokenMap = {}) {
|
|
857
|
+
export function restoreObject(obj, tokenMap = {}, _seen = null) {
|
|
785
858
|
if (obj === null || obj === undefined) return obj;
|
|
786
859
|
if (typeof obj === 'string') {
|
|
787
860
|
const res = restore(obj, tokenMap);
|
|
788
861
|
return res.restoredText;
|
|
789
862
|
}
|
|
863
|
+
if (typeof obj !== 'object') return obj;
|
|
864
|
+
if (
|
|
865
|
+
obj instanceof Date ||
|
|
866
|
+
obj instanceof RegExp ||
|
|
867
|
+
obj instanceof Error ||
|
|
868
|
+
obj instanceof Promise ||
|
|
869
|
+
obj instanceof WeakMap ||
|
|
870
|
+
obj instanceof WeakSet ||
|
|
871
|
+
(typeof URL !== 'undefined' && obj instanceof URL) ||
|
|
872
|
+
ArrayBuffer.isView(obj) ||
|
|
873
|
+
(typeof ArrayBuffer !== 'undefined' && obj instanceof ArrayBuffer)
|
|
874
|
+
) {
|
|
875
|
+
return obj;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const seen = _seen || new WeakMap();
|
|
879
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
880
|
+
|
|
790
881
|
if (Array.isArray(obj)) {
|
|
791
|
-
|
|
882
|
+
const outArr = [];
|
|
883
|
+
seen.set(obj, outArr);
|
|
884
|
+
for (let i = 0; i < obj.length; i++) {
|
|
885
|
+
outArr.push(restoreObject(obj[i], tokenMap, seen));
|
|
886
|
+
}
|
|
887
|
+
return outArr;
|
|
792
888
|
}
|
|
793
|
-
if (
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
889
|
+
if (obj instanceof Map) {
|
|
890
|
+
const outMap = new Map();
|
|
891
|
+
seen.set(obj, outMap);
|
|
892
|
+
for (const [k, v] of obj.entries()) {
|
|
893
|
+
outMap.set(k, restoreObject(v, tokenMap, seen));
|
|
797
894
|
}
|
|
798
|
-
return
|
|
895
|
+
return outMap;
|
|
896
|
+
}
|
|
897
|
+
if (obj instanceof Set) {
|
|
898
|
+
const outSet = new Set();
|
|
899
|
+
seen.set(obj, outSet);
|
|
900
|
+
for (const item of obj.values()) {
|
|
901
|
+
outSet.add(restoreObject(item, tokenMap, seen));
|
|
902
|
+
}
|
|
903
|
+
return outSet;
|
|
799
904
|
}
|
|
800
|
-
|
|
905
|
+
|
|
906
|
+
const out = {};
|
|
907
|
+
seen.set(obj, out);
|
|
908
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
909
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
910
|
+
out[k] = restoreObject(v, tokenMap, seen);
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
801
913
|
}
|
|
802
914
|
|
|
803
915
|
/**
|
|
@@ -840,12 +952,16 @@ export class PrivacyScrubberEngine {
|
|
|
840
952
|
.forEach(v => mergedIgnoreList.add(v));
|
|
841
953
|
}
|
|
842
954
|
|
|
955
|
+
const effectiveExistingMap = options.persistSession !== false
|
|
956
|
+
? { ...this.sessionMap, ...(options.existingSessionMap || {}) }
|
|
957
|
+
: (options.existingSessionMap || {});
|
|
958
|
+
|
|
843
959
|
const mergedOptions = {
|
|
844
960
|
profile: options.profile || this.defaultProfile,
|
|
845
961
|
licenseKey: options.licenseKey || this.licenseKey,
|
|
846
962
|
customRules: [...this.customRules, ...(options.customRules || [])],
|
|
847
963
|
tokenLabelMap: { ...this.tokenLabelMap, ...(options.tokenLabelMap || {}) },
|
|
848
|
-
existingSessionMap:
|
|
964
|
+
existingSessionMap: effectiveExistingMap,
|
|
849
965
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
850
966
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
851
967
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
@@ -945,68 +1061,229 @@ export class PrivacyScrubberEngine {
|
|
|
945
1061
|
* @returns {object} Wrapped OpenAI client.
|
|
946
1062
|
*/
|
|
947
1063
|
export function wrapOpenAI(openaiClient, options = {}) {
|
|
948
|
-
|
|
1064
|
+
const hasChatCompletions = !!(openaiClient && openaiClient.chat && openaiClient.chat.completions && typeof openaiClient.chat.completions.create === 'function');
|
|
1065
|
+
const hasResponses = !!(openaiClient && openaiClient.responses && typeof openaiClient.responses.create === 'function');
|
|
1066
|
+
if (!hasChatCompletions && !hasResponses) {
|
|
949
1067
|
throw new Error('[PrivacyScrubber] Invalid OpenAI client instance passed to wrapOpenAI.');
|
|
950
1068
|
}
|
|
951
1069
|
|
|
952
1070
|
const engine = new PrivacyScrubberEngine(options);
|
|
953
|
-
const originalCreate = openaiClient.chat.completions.create.bind(openaiClient.chat.completions);
|
|
954
1071
|
|
|
955
|
-
|
|
956
|
-
if (
|
|
957
|
-
|
|
1072
|
+
function sanitizeMessageContent(content, sessionTokenMap) {
|
|
1073
|
+
if (typeof content === 'string') {
|
|
1074
|
+
const res = engine.sanitize(content, { existingSessionMap: sessionTokenMap });
|
|
1075
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1076
|
+
return res.scrubbedText;
|
|
958
1077
|
}
|
|
1078
|
+
if (Array.isArray(content)) {
|
|
1079
|
+
return content.map(chunk => {
|
|
1080
|
+
if (chunk && (chunk.type === 'text' || chunk.type === 'input_text') && typeof chunk.text === 'string') {
|
|
1081
|
+
const res = engine.sanitize(chunk.text, { existingSessionMap: sessionTokenMap });
|
|
1082
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1083
|
+
return { ...chunk, text: res.scrubbedText };
|
|
1084
|
+
}
|
|
1085
|
+
return chunk;
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return content;
|
|
1089
|
+
}
|
|
959
1090
|
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
1091
|
+
if (hasChatCompletions) {
|
|
1092
|
+
const originalCreate = openaiClient.chat.completions.create.bind(openaiClient.chat.completions);
|
|
1093
|
+
|
|
1094
|
+
openaiClient.chat.completions.create = async function(params, requestOptions) {
|
|
1095
|
+
if (!params || !Array.isArray(params.messages)) {
|
|
1096
|
+
return originalCreate(params, requestOptions);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const sessionTokenMap = options.persistSession === false ? {} : { ...engine.sessionMap };
|
|
1100
|
+
const sanitizedMessages = params.messages.map(msg => ({
|
|
1101
|
+
...msg,
|
|
1102
|
+
content: sanitizeMessageContent(msg.content, sessionTokenMap)
|
|
1103
|
+
}));
|
|
1104
|
+
|
|
1105
|
+
const modifiedParams = { ...params, messages: sanitizedMessages };
|
|
1106
|
+
const response = await originalCreate(modifiedParams, requestOptions);
|
|
1107
|
+
|
|
1108
|
+
// Streaming support: transparently de-tokenize async iterable chunks
|
|
1109
|
+
if (params.stream) {
|
|
1110
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1111
|
+
return detok.wrapStream(response);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (response && Array.isArray(response.choices)) {
|
|
1115
|
+
response.choices.forEach(choice => {
|
|
1116
|
+
if (choice.message) {
|
|
1117
|
+
if (typeof choice.message.content === 'string') {
|
|
1118
|
+
choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
|
|
1119
|
+
} else if (Array.isArray(choice.message.content)) {
|
|
1120
|
+
choice.message.content = choice.message.content.map(chunk => {
|
|
1121
|
+
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
1122
|
+
return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
|
|
1123
|
+
}
|
|
1124
|
+
return chunk;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return response;
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (hasResponses) {
|
|
1136
|
+
const originalResponsesCreate = openaiClient.responses.create.bind(openaiClient.responses);
|
|
1137
|
+
|
|
1138
|
+
openaiClient.responses.create = async function(params, requestOptions) {
|
|
1139
|
+
if (!params || typeof params !== 'object') {
|
|
1140
|
+
return originalResponsesCreate(params, requestOptions);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const sessionTokenMap = options.persistSession === false ? {} : { ...engine.sessionMap };
|
|
1144
|
+
const modifiedParams = { ...params };
|
|
1145
|
+
|
|
1146
|
+
if (typeof modifiedParams.instructions === 'string') {
|
|
1147
|
+
const res = engine.sanitize(modifiedParams.instructions, { existingSessionMap: sessionTokenMap });
|
|
964
1148
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
965
|
-
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1149
|
+
modifiedParams.instructions = res.scrubbedText;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (typeof modifiedParams.input === 'string') {
|
|
1153
|
+
const res = engine.sanitize(modifiedParams.input, { existingSessionMap: sessionTokenMap });
|
|
1154
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1155
|
+
modifiedParams.input = res.scrubbedText;
|
|
1156
|
+
} else if (Array.isArray(modifiedParams.input)) {
|
|
1157
|
+
modifiedParams.input = modifiedParams.input.map(item => {
|
|
1158
|
+
if (typeof item === 'string') {
|
|
1159
|
+
const res = engine.sanitize(item, { existingSessionMap: sessionTokenMap });
|
|
970
1160
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
971
|
-
return
|
|
1161
|
+
return res.scrubbedText;
|
|
1162
|
+
}
|
|
1163
|
+
if (item && typeof item === 'object') {
|
|
1164
|
+
return {
|
|
1165
|
+
...item,
|
|
1166
|
+
...(item.content !== undefined && { content: sanitizeMessageContent(item.content, sessionTokenMap) }),
|
|
1167
|
+
...(typeof item.text === 'string' && {
|
|
1168
|
+
text: (() => {
|
|
1169
|
+
const res = engine.sanitize(item.text, { existingSessionMap: sessionTokenMap });
|
|
1170
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1171
|
+
return res.scrubbedText;
|
|
1172
|
+
})()
|
|
1173
|
+
})
|
|
1174
|
+
};
|
|
972
1175
|
}
|
|
973
|
-
return
|
|
1176
|
+
return item;
|
|
974
1177
|
});
|
|
975
|
-
return { ...msg, content: sanitizedChunks };
|
|
976
1178
|
}
|
|
977
|
-
return msg;
|
|
978
|
-
});
|
|
979
1179
|
|
|
980
|
-
|
|
1180
|
+
const response = await originalResponsesCreate(modifiedParams, requestOptions);
|
|
1181
|
+
|
|
1182
|
+
if (params.stream) {
|
|
1183
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1184
|
+
return detok.wrapStream(response);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (response && typeof response === 'object') {
|
|
1188
|
+
if (typeof response.output_text === 'string') {
|
|
1189
|
+
try {
|
|
1190
|
+
response.output_text = engine.restore(response.output_text, sessionTokenMap).restoredText;
|
|
1191
|
+
} catch (_) {}
|
|
1192
|
+
}
|
|
1193
|
+
if (Array.isArray(response.output)) {
|
|
1194
|
+
response.output.forEach(outItem => {
|
|
1195
|
+
if (outItem && Array.isArray(outItem.content)) {
|
|
1196
|
+
outItem.content.forEach(block => {
|
|
1197
|
+
if (block && typeof block.text === 'string') {
|
|
1198
|
+
block.text = engine.restore(block.text, sessionTokenMap).restoredText;
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return response;
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return openaiClient;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Transparent Zero-Trust wrapper for Anthropic SDK (@anthropic-ai/sdk).
|
|
1215
|
+
* Automatically scrubs system prompts and user/assistant messages in local RAM
|
|
1216
|
+
* before sending to Anthropic, and restores masked tokens in both synchronous
|
|
1217
|
+
* and streaming responses.
|
|
1218
|
+
*
|
|
1219
|
+
* @param {object} anthropicClient - Instance of Anthropic SDK (e.g. new Anthropic()).
|
|
1220
|
+
* @param {object} [options={}] - PrivacyScrubber configuration.
|
|
1221
|
+
* @returns {object} Wrapped Anthropic client.
|
|
1222
|
+
*/
|
|
1223
|
+
export function wrapAnthropic(anthropicClient, options = {}) {
|
|
1224
|
+
if (!anthropicClient || !anthropicClient.messages || typeof anthropicClient.messages.create !== 'function') {
|
|
1225
|
+
throw new Error('[PrivacyScrubber] Invalid Anthropic client instance passed to wrapAnthropic.');
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const engine = new PrivacyScrubberEngine(options);
|
|
1229
|
+
const originalCreate = anthropicClient.messages.create.bind(anthropicClient.messages);
|
|
1230
|
+
|
|
1231
|
+
function sanitizeAnthropicContent(content, sessionTokenMap) {
|
|
1232
|
+
if (typeof content === 'string') {
|
|
1233
|
+
const res = engine.sanitize(content, { existingSessionMap: sessionTokenMap });
|
|
1234
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1235
|
+
return res.scrubbedText;
|
|
1236
|
+
}
|
|
1237
|
+
if (Array.isArray(content)) {
|
|
1238
|
+
return content.map(block => {
|
|
1239
|
+
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
1240
|
+
const res = engine.sanitize(block.text, { existingSessionMap: sessionTokenMap });
|
|
1241
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1242
|
+
return { ...block, text: res.scrubbedText };
|
|
1243
|
+
}
|
|
1244
|
+
return block;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
return content;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
anthropicClient.messages.create = async function(params, requestOptions) {
|
|
1251
|
+
if (!params || !Array.isArray(params.messages)) {
|
|
1252
|
+
return originalCreate(params, requestOptions);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const sessionTokenMap = {};
|
|
1256
|
+
const modifiedParams = { ...params };
|
|
1257
|
+
|
|
1258
|
+
if (modifiedParams.system !== undefined) {
|
|
1259
|
+
modifiedParams.system = sanitizeAnthropicContent(modifiedParams.system, sessionTokenMap);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
modifiedParams.messages = params.messages.map(msg => ({
|
|
1263
|
+
...msg,
|
|
1264
|
+
content: sanitizeAnthropicContent(msg.content, sessionTokenMap)
|
|
1265
|
+
}));
|
|
1266
|
+
|
|
981
1267
|
const response = await originalCreate(modifiedParams, requestOptions);
|
|
982
1268
|
|
|
983
|
-
// Streaming support: transparently de-tokenize async iterable chunks
|
|
984
1269
|
if (params.stream) {
|
|
985
1270
|
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
986
1271
|
return detok.wrapStream(response);
|
|
987
1272
|
}
|
|
988
1273
|
|
|
989
|
-
if (response && Array.isArray(response.
|
|
990
|
-
response.
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
|
|
994
|
-
} else if (Array.isArray(choice.message.content)) {
|
|
995
|
-
choice.message.content = choice.message.content.map(chunk => {
|
|
996
|
-
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
997
|
-
return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
|
|
998
|
-
}
|
|
999
|
-
return chunk;
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1274
|
+
if (response && Array.isArray(response.content)) {
|
|
1275
|
+
response.content = response.content.map(block => {
|
|
1276
|
+
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
1277
|
+
return { ...block, text: engine.restore(block.text, sessionTokenMap).restoredText };
|
|
1002
1278
|
}
|
|
1279
|
+
return block;
|
|
1003
1280
|
});
|
|
1004
1281
|
}
|
|
1005
1282
|
|
|
1006
1283
|
return response;
|
|
1007
1284
|
};
|
|
1008
1285
|
|
|
1009
|
-
return
|
|
1286
|
+
return anthropicClient;
|
|
1010
1287
|
}
|
|
1011
1288
|
|
|
1012
1289
|
/**
|
|
@@ -1025,25 +1302,43 @@ export function wrapAiModel(model, options = {}) {
|
|
|
1025
1302
|
throw new Error('[PrivacyScrubber] Invalid model instance passed to wrapAiModel.');
|
|
1026
1303
|
}
|
|
1027
1304
|
|
|
1028
|
-
const engine = new PrivacyScrubberEngine(options);
|
|
1305
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1306
|
+
const sharedSessionMap = {};
|
|
1029
1307
|
const wrapped = Object.create(model);
|
|
1030
1308
|
|
|
1031
1309
|
function sanitizePromptParts(prompt, sessionMap) {
|
|
1310
|
+
if (typeof prompt === 'string') {
|
|
1311
|
+
const res = engine.sanitize(prompt, { existingSessionMap: sessionMap, persistSession: false });
|
|
1312
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1313
|
+
return res.scrubbedText;
|
|
1314
|
+
}
|
|
1032
1315
|
if (!Array.isArray(prompt)) return prompt;
|
|
1033
1316
|
return prompt.map(msg => {
|
|
1034
1317
|
if (!msg) return msg;
|
|
1035
1318
|
if (typeof msg.content === 'string') {
|
|
1036
|
-
const res = engine.sanitize(msg.content, { existingSessionMap: sessionMap });
|
|
1319
|
+
const res = engine.sanitize(msg.content, { existingSessionMap: sessionMap, persistSession: false });
|
|
1037
1320
|
Object.assign(sessionMap, res.tokenMap);
|
|
1038
1321
|
return { ...msg, content: res.scrubbedText };
|
|
1039
1322
|
}
|
|
1040
1323
|
if (Array.isArray(msg.content)) {
|
|
1041
1324
|
const newContent = msg.content.map(part => {
|
|
1042
|
-
if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
1043
|
-
const res = engine.sanitize(part.text, { existingSessionMap: sessionMap });
|
|
1325
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
1326
|
+
const res = engine.sanitize(part.text, { existingSessionMap: sessionMap, persistSession: false });
|
|
1044
1327
|
Object.assign(sessionMap, res.tokenMap);
|
|
1045
1328
|
return { ...part, text: res.scrubbedText };
|
|
1046
1329
|
}
|
|
1330
|
+
if (part && part.type === 'tool-result') {
|
|
1331
|
+
if (typeof part.result === 'string') {
|
|
1332
|
+
const res = engine.sanitize(part.result, { existingSessionMap: sessionMap, persistSession: false });
|
|
1333
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1334
|
+
return { ...part, result: res.scrubbedText };
|
|
1335
|
+
}
|
|
1336
|
+
if (part.output && typeof part.output.value === 'string') {
|
|
1337
|
+
const res = engine.sanitize(part.output.value, { existingSessionMap: sessionMap, persistSession: false });
|
|
1338
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1339
|
+
return { ...part, output: { ...part.output, value: res.scrubbedText } };
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1047
1342
|
return part;
|
|
1048
1343
|
});
|
|
1049
1344
|
return { ...msg, content: newContent };
|
|
@@ -1052,16 +1347,41 @@ export function wrapAiModel(model, options = {}) {
|
|
|
1052
1347
|
});
|
|
1053
1348
|
}
|
|
1054
1349
|
|
|
1350
|
+
function createScopedTokenMap(prompt) {
|
|
1351
|
+
const sessionTokenMap = options.persistSession ? { ...sharedSessionMap } : {};
|
|
1352
|
+
if (!options.persistSession && prompt && Object.keys(sharedSessionMap).length > 0) {
|
|
1353
|
+
const s = typeof prompt === 'string' ? prompt : JSON.stringify(prompt);
|
|
1354
|
+
for (const [tok, orig] of Object.entries(sharedSessionMap)) {
|
|
1355
|
+
if (s.includes(tok)) sessionTokenMap[tok] = orig;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return sessionTokenMap;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1055
1361
|
if (typeof model.doGenerate === 'function') {
|
|
1056
1362
|
wrapped.doGenerate = async function(params) {
|
|
1057
|
-
const sessionTokenMap =
|
|
1363
|
+
const sessionTokenMap = createScopedTokenMap(params?.prompt);
|
|
1058
1364
|
const sanitizedParams = {
|
|
1059
1365
|
...params,
|
|
1060
1366
|
prompt: sanitizePromptParts(params.prompt, sessionTokenMap)
|
|
1061
1367
|
};
|
|
1368
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1062
1369
|
const result = await model.doGenerate(sanitizedParams);
|
|
1063
|
-
if (result
|
|
1064
|
-
|
|
1370
|
+
if (result) {
|
|
1371
|
+
if (typeof result.text === 'string') {
|
|
1372
|
+
result.text = engine.restore(result.text, sessionTokenMap).restoredText;
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof result.reasoning === 'string') {
|
|
1375
|
+
result.reasoning = engine.restore(result.reasoning, sessionTokenMap).restoredText;
|
|
1376
|
+
}
|
|
1377
|
+
if (Array.isArray(result.content)) {
|
|
1378
|
+
result.content = result.content.map(part => {
|
|
1379
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
1380
|
+
return { ...part, text: engine.restore(part.text, sessionTokenMap).restoredText };
|
|
1381
|
+
}
|
|
1382
|
+
return part;
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1065
1385
|
}
|
|
1066
1386
|
return result;
|
|
1067
1387
|
};
|
|
@@ -1069,43 +1389,87 @@ export function wrapAiModel(model, options = {}) {
|
|
|
1069
1389
|
|
|
1070
1390
|
if (typeof model.doStream === 'function') {
|
|
1071
1391
|
wrapped.doStream = async function(params) {
|
|
1072
|
-
const sessionTokenMap =
|
|
1392
|
+
const sessionTokenMap = createScopedTokenMap(params?.prompt);
|
|
1073
1393
|
const sanitizedParams = {
|
|
1074
1394
|
...params,
|
|
1075
1395
|
prompt: sanitizePromptParts(params.prompt, sessionTokenMap)
|
|
1076
1396
|
};
|
|
1397
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1077
1398
|
const result = await model.doStream(sanitizedParams);
|
|
1078
1399
|
if (result && result.stream) {
|
|
1079
1400
|
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1080
1401
|
const originalStream = result.stream;
|
|
1402
|
+
let usesV2Delta = false;
|
|
1081
1403
|
|
|
1082
1404
|
if (typeof originalStream.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
1083
1405
|
result.stream = originalStream.pipeThrough(new TransformStream({
|
|
1084
1406
|
transform(chunk, controller) {
|
|
1085
|
-
if (chunk && chunk.type === 'text-delta'
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1407
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
1408
|
+
if (typeof chunk.textDelta === 'string') {
|
|
1409
|
+
const restored = detok.feed(chunk.textDelta);
|
|
1410
|
+
if (restored) {
|
|
1411
|
+
controller.enqueue({
|
|
1412
|
+
...chunk,
|
|
1413
|
+
textDelta: restored,
|
|
1414
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof chunk.delta === 'string') {
|
|
1420
|
+
usesV2Delta = true;
|
|
1421
|
+
const restored = detok.feed(chunk.delta);
|
|
1422
|
+
if (restored) {
|
|
1423
|
+
controller.enqueue({ ...chunk, delta: restored, textDelta: restored });
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1090
1427
|
}
|
|
1428
|
+
controller.enqueue(chunk);
|
|
1091
1429
|
},
|
|
1092
1430
|
flush(controller) {
|
|
1093
1431
|
const rem = detok.flush();
|
|
1094
|
-
if (rem)
|
|
1432
|
+
if (rem) {
|
|
1433
|
+
controller.enqueue(
|
|
1434
|
+
usesV2Delta
|
|
1435
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
1436
|
+
: { type: 'text-delta', textDelta: rem, delta: rem }
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1095
1439
|
}
|
|
1096
1440
|
}));
|
|
1097
1441
|
} else if (typeof originalStream[Symbol.asyncIterator] === 'function') {
|
|
1098
1442
|
async function* wrappedAsyncIterable() {
|
|
1099
1443
|
for await (const chunk of originalStream) {
|
|
1100
|
-
if (chunk && chunk.type === 'text-delta'
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1444
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
1445
|
+
if (typeof chunk.textDelta === 'string') {
|
|
1446
|
+
const restored = detok.feed(chunk.textDelta);
|
|
1447
|
+
if (restored) {
|
|
1448
|
+
yield {
|
|
1449
|
+
...chunk,
|
|
1450
|
+
textDelta: restored,
|
|
1451
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (typeof chunk.delta === 'string') {
|
|
1457
|
+
usesV2Delta = true;
|
|
1458
|
+
const restored = detok.feed(chunk.delta);
|
|
1459
|
+
if (restored) {
|
|
1460
|
+
yield { ...chunk, delta: restored, textDelta: restored };
|
|
1461
|
+
}
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1105
1464
|
}
|
|
1465
|
+
yield chunk;
|
|
1106
1466
|
}
|
|
1107
1467
|
const rem = detok.flush();
|
|
1108
|
-
if (rem)
|
|
1468
|
+
if (rem) {
|
|
1469
|
+
yield usesV2Delta
|
|
1470
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
1471
|
+
: { type: 'text-delta', textDelta: rem, delta: rem };
|
|
1472
|
+
}
|
|
1109
1473
|
}
|
|
1110
1474
|
result.stream = wrappedAsyncIterable();
|
|
1111
1475
|
}
|
|
@@ -1119,6 +1483,631 @@ export function wrapAiModel(model, options = {}) {
|
|
|
1119
1483
|
|
|
1120
1484
|
export const wrapAiSdk = wrapAiModel;
|
|
1121
1485
|
|
|
1486
|
+
/**
|
|
1487
|
+
* Transparent Zero-Trust wrapper for Google Gemini SDKs:
|
|
1488
|
+
* - Official @google/genai SDK (GoogleGenAI instance with client.models.generateContent / generateContentStream)
|
|
1489
|
+
* - Legacy @google/generative-ai SDK (GenerativeModel or GoogleGenerativeAI instance)
|
|
1490
|
+
*
|
|
1491
|
+
* Automatically scrubs string, Part[], and Content[] prompts as well as systemInstruction in local RAM,
|
|
1492
|
+
* and restores masked tokens in both synchronous and streaming responses.
|
|
1493
|
+
*
|
|
1494
|
+
* @param {object} geminiClientOrModel - GoogleGenAI instance, GenerativeModel instance, or GoogleGenerativeAI client.
|
|
1495
|
+
* @param {object} [options={}] - PrivacyScrubber configuration options.
|
|
1496
|
+
* @returns {object} Wrapped Gemini client or model.
|
|
1497
|
+
*/
|
|
1498
|
+
export function wrapGemini(geminiClientOrModel, options = {}) {
|
|
1499
|
+
if (!geminiClientOrModel || typeof geminiClientOrModel !== 'object') {
|
|
1500
|
+
throw new Error('[PrivacyScrubber] Invalid Gemini client or model passed to wrapGemini.');
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1504
|
+
const sharedSessionMap = {};
|
|
1505
|
+
|
|
1506
|
+
function sanitizeGeminiContentNode(node, sessionTokenMap) {
|
|
1507
|
+
if (node === null || node === undefined) return node;
|
|
1508
|
+
if (typeof node === 'string') {
|
|
1509
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1510
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1511
|
+
return res.scrubbedText;
|
|
1512
|
+
}
|
|
1513
|
+
if (Array.isArray(node)) {
|
|
1514
|
+
return node.map(item => sanitizeGeminiContentNode(item, sessionTokenMap));
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof node === 'object') {
|
|
1517
|
+
const copy = { ...node };
|
|
1518
|
+
if (typeof copy.text === 'string') {
|
|
1519
|
+
const res = engine.sanitize(copy.text, { existingSessionMap: sessionTokenMap });
|
|
1520
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1521
|
+
copy.text = res.scrubbedText;
|
|
1522
|
+
}
|
|
1523
|
+
if (Array.isArray(copy.parts)) {
|
|
1524
|
+
copy.parts = copy.parts.map(p => sanitizeGeminiContentNode(p, sessionTokenMap));
|
|
1525
|
+
}
|
|
1526
|
+
if (copy.contents !== undefined) {
|
|
1527
|
+
copy.contents = sanitizeGeminiContentNode(copy.contents, sessionTokenMap);
|
|
1528
|
+
}
|
|
1529
|
+
if (copy.systemInstruction !== undefined) {
|
|
1530
|
+
copy.systemInstruction = sanitizeGeminiContentNode(copy.systemInstruction, sessionTokenMap);
|
|
1531
|
+
}
|
|
1532
|
+
if (copy.config && typeof copy.config === 'object') {
|
|
1533
|
+
copy.config = { ...copy.config };
|
|
1534
|
+
if (copy.config.systemInstruction !== undefined) {
|
|
1535
|
+
copy.config.systemInstruction = sanitizeGeminiContentNode(copy.config.systemInstruction, sessionTokenMap);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return copy;
|
|
1539
|
+
}
|
|
1540
|
+
return node;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function restoreGeminiResponseObject(respObj, sessionTokenMap, detok = null) {
|
|
1544
|
+
if (!respObj || typeof respObj !== 'object') return respObj;
|
|
1545
|
+
const restoreFn = (str) => detok ? detok.feed(str) : engine.restore(str, sessionTokenMap).restoredText;
|
|
1546
|
+
|
|
1547
|
+
let combinedText = '';
|
|
1548
|
+
if (Array.isArray(respObj.candidates)) {
|
|
1549
|
+
respObj.candidates = respObj.candidates.map(cand => {
|
|
1550
|
+
if (!cand || typeof cand !== 'object') return cand;
|
|
1551
|
+
const candCopy = { ...cand };
|
|
1552
|
+
if (candCopy.content && Array.isArray(candCopy.content.parts)) {
|
|
1553
|
+
candCopy.content = {
|
|
1554
|
+
...candCopy.content,
|
|
1555
|
+
parts: candCopy.content.parts.map(part => {
|
|
1556
|
+
if (part && typeof part.text === 'string') {
|
|
1557
|
+
const restoredText = restoreFn(part.text);
|
|
1558
|
+
combinedText += restoredText;
|
|
1559
|
+
return { ...part, text: restoredText };
|
|
1560
|
+
}
|
|
1561
|
+
return part;
|
|
1562
|
+
})
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
return candCopy;
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (typeof respObj.text === 'function') {
|
|
1570
|
+
const origTextFn = respObj.text.bind(respObj);
|
|
1571
|
+
respObj.text = () => {
|
|
1572
|
+
if (combinedText) return combinedText;
|
|
1573
|
+
const raw = origTextFn();
|
|
1574
|
+
return typeof raw === 'string' ? restoreFn(raw) : raw;
|
|
1575
|
+
};
|
|
1576
|
+
} else if (typeof respObj.text === 'string') {
|
|
1577
|
+
const restored = combinedText || restoreFn(respObj.text);
|
|
1578
|
+
try {
|
|
1579
|
+
Object.defineProperty(respObj, 'text', {
|
|
1580
|
+
value: restored,
|
|
1581
|
+
writable: true,
|
|
1582
|
+
enumerable: true,
|
|
1583
|
+
configurable: true
|
|
1584
|
+
});
|
|
1585
|
+
} catch (_) {
|
|
1586
|
+
respObj.text = restored;
|
|
1587
|
+
}
|
|
1588
|
+
} else if (combinedText) {
|
|
1589
|
+
try {
|
|
1590
|
+
Object.defineProperty(respObj, 'text', {
|
|
1591
|
+
value: combinedText,
|
|
1592
|
+
writable: true,
|
|
1593
|
+
enumerable: true,
|
|
1594
|
+
configurable: true
|
|
1595
|
+
});
|
|
1596
|
+
} catch (_) {}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (respObj.response && typeof respObj.response === 'object') {
|
|
1600
|
+
restoreGeminiResponseObject(respObj.response, sessionTokenMap, detok);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return respObj;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function wrapGeminiStreamResult(streamResult, sessionTokenMap) {
|
|
1607
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1608
|
+
|
|
1609
|
+
async function* wrapAsyncChunks(iterable) {
|
|
1610
|
+
let lastChunkTemplate = null;
|
|
1611
|
+
for await (const chunk of iterable) {
|
|
1612
|
+
if (!chunk) continue;
|
|
1613
|
+
lastChunkTemplate = chunk;
|
|
1614
|
+
if (typeof chunk === 'string') {
|
|
1615
|
+
const out = detok.feed(chunk);
|
|
1616
|
+
if (out) yield out;
|
|
1617
|
+
} else if (typeof chunk === 'object') {
|
|
1618
|
+
const cloned = Object.create(Object.getPrototypeOf(chunk));
|
|
1619
|
+
Object.assign(cloned, chunk);
|
|
1620
|
+
restoreGeminiResponseObject(cloned, sessionTokenMap, detok);
|
|
1621
|
+
yield cloned;
|
|
1622
|
+
} else {
|
|
1623
|
+
yield chunk;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const tail = detok.flush();
|
|
1627
|
+
if (tail) {
|
|
1628
|
+
if (lastChunkTemplate && typeof lastChunkTemplate === 'object') {
|
|
1629
|
+
const finalChunk = {
|
|
1630
|
+
...lastChunkTemplate,
|
|
1631
|
+
candidates: [{ content: { role: 'model', parts: [{ text: tail }] } }],
|
|
1632
|
+
text: tail
|
|
1633
|
+
};
|
|
1634
|
+
if (typeof lastChunkTemplate.text === 'function') {
|
|
1635
|
+
finalChunk.text = () => tail;
|
|
1636
|
+
}
|
|
1637
|
+
yield finalChunk;
|
|
1638
|
+
} else {
|
|
1639
|
+
yield tail;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (streamResult && typeof streamResult[Symbol.asyncIterator] === 'function') {
|
|
1645
|
+
return wrapAsyncChunks(streamResult);
|
|
1646
|
+
}
|
|
1647
|
+
if (streamResult && streamResult.stream && typeof streamResult.stream[Symbol.asyncIterator] === 'function') {
|
|
1648
|
+
const wrappedStream = wrapAsyncChunks(streamResult.stream);
|
|
1649
|
+
const wrappedResponse = streamResult.response && typeof streamResult.response.then === 'function'
|
|
1650
|
+
? streamResult.response.then(r => restoreGeminiResponseObject(r, sessionTokenMap))
|
|
1651
|
+
: streamResult.response;
|
|
1652
|
+
return {
|
|
1653
|
+
...streamResult,
|
|
1654
|
+
stream: wrappedStream,
|
|
1655
|
+
response: wrappedResponse
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
return streamResult;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function wrapTargetObject(target) {
|
|
1662
|
+
if (typeof target.generateContent === 'function') {
|
|
1663
|
+
const origGen = target.generateContent.bind(target);
|
|
1664
|
+
target.generateContent = async function(req, ...rest) {
|
|
1665
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1666
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1667
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1668
|
+
const res = await origGen(sanitizedReq, ...rest);
|
|
1669
|
+
return restoreGeminiResponseObject(res, sessionTokenMap);
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof target.generateContentStream === 'function') {
|
|
1673
|
+
const origStream = target.generateContentStream.bind(target);
|
|
1674
|
+
target.generateContentStream = async function(req, ...rest) {
|
|
1675
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1676
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1677
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1678
|
+
const streamRes = await origStream(sanitizedReq, ...rest);
|
|
1679
|
+
return wrapGeminiStreamResult(streamRes, sessionTokenMap);
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (geminiClientOrModel.models && typeof geminiClientOrModel.models === 'object') {
|
|
1685
|
+
wrapTargetObject(geminiClientOrModel.models);
|
|
1686
|
+
}
|
|
1687
|
+
if (typeof geminiClientOrModel.generateContent === 'function' || typeof geminiClientOrModel.generateContentStream === 'function') {
|
|
1688
|
+
wrapTargetObject(geminiClientOrModel);
|
|
1689
|
+
}
|
|
1690
|
+
if (typeof geminiClientOrModel.getGenerativeModel === 'function') {
|
|
1691
|
+
const origGetModel = geminiClientOrModel.getGenerativeModel.bind(geminiClientOrModel);
|
|
1692
|
+
geminiClientOrModel.getGenerativeModel = function(modelParams, ...rest) {
|
|
1693
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1694
|
+
const sanitizedParams = sanitizeGeminiContentNode(modelParams, sessionTokenMap);
|
|
1695
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1696
|
+
const modelInstance = origGetModel(sanitizedParams, ...rest);
|
|
1697
|
+
if (modelInstance && typeof modelInstance === 'object') {
|
|
1698
|
+
wrapTargetObject(modelInstance);
|
|
1699
|
+
}
|
|
1700
|
+
return modelInstance;
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
geminiClientOrModel._ztdsEngine = engine;
|
|
1705
|
+
return geminiClientOrModel;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
export const wrapGoogleGenAI = wrapGemini;
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Creates a universal Zero-Trust HTTP/SSE `fetch` wrapper for any LLM provider or SDK
|
|
1712
|
+
* (OpenAI, Anthropic, OpenRouter, Groq, DeepSeek, Mistral, Together, Ollama, Cloudflare AI Gateway).
|
|
1713
|
+
*
|
|
1714
|
+
* Automatically sanitizes outbound JSON request bodies (`messages`, `prompt`, `input`, `contents`, `system`)
|
|
1715
|
+
* in local RAM before network transmission, and transparently de-tokenizes both JSON responses
|
|
1716
|
+
* and Server-Sent Events (`text/event-stream`) streams in real time.
|
|
1717
|
+
*
|
|
1718
|
+
* Usage:
|
|
1719
|
+
* const client = new OpenAI({ fetch: createPrivacyFetch({ profile: 'General' }) });
|
|
1720
|
+
*
|
|
1721
|
+
* @param {object} [options={}] - Configuration options.
|
|
1722
|
+
* @returns {Function} Drop-in `fetch(input, init)` replacement.
|
|
1723
|
+
*/
|
|
1724
|
+
export function createPrivacyFetch(options = {}) {
|
|
1725
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1726
|
+
const baseFetch = options.fetch || (typeof globalThis !== 'undefined' && globalThis.fetch);
|
|
1727
|
+
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
1728
|
+
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
1729
|
+
|
|
1730
|
+
const PROMPT_KEYS = new Set([
|
|
1731
|
+
'messages', 'prompt', 'input', 'contents', 'system',
|
|
1732
|
+
'systemInstruction', 'system_instruction', 'instructions', 'text', 'content'
|
|
1733
|
+
]);
|
|
1734
|
+
const SAFE_SKIP_KEYS = new Set([
|
|
1735
|
+
'model', 'role', 'type', 'id', 'object', 'finish_reason', 'stop_reason',
|
|
1736
|
+
'encoding_format', 'response_format', 'tool_choice', 'stream'
|
|
1737
|
+
]);
|
|
1738
|
+
|
|
1739
|
+
function sanitizeJsonNode(node, sessionTokenMap) {
|
|
1740
|
+
if (node === null || node === undefined) return node;
|
|
1741
|
+
if (typeof node === 'string') {
|
|
1742
|
+
if (!node.trim()) return node;
|
|
1743
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1744
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1745
|
+
return res.scrubbedText;
|
|
1746
|
+
}
|
|
1747
|
+
if (Array.isArray(node)) {
|
|
1748
|
+
return node.map(item => sanitizeJsonNode(item, sessionTokenMap));
|
|
1749
|
+
}
|
|
1750
|
+
if (typeof node === 'object') {
|
|
1751
|
+
const out = {};
|
|
1752
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1753
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
1754
|
+
if (SAFE_SKIP_KEYS.has(k)) {
|
|
1755
|
+
out[k] = v;
|
|
1756
|
+
} else if (PROMPT_KEYS.has(k) || typeof v === 'object') {
|
|
1757
|
+
out[k] = sanitizeJsonNode(v, sessionTokenMap);
|
|
1758
|
+
} else {
|
|
1759
|
+
out[k] = v;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return out;
|
|
1763
|
+
}
|
|
1764
|
+
return node;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function detokenizeSseJsonPayload(parsed, detok) {
|
|
1768
|
+
if (!parsed || typeof parsed !== 'object') return parsed;
|
|
1769
|
+
if (Array.isArray(parsed.choices)) {
|
|
1770
|
+
parsed.choices = parsed.choices.map(choice => {
|
|
1771
|
+
if (!choice || typeof choice !== 'object') return choice;
|
|
1772
|
+
const c = { ...choice };
|
|
1773
|
+
if (c.delta && typeof c.delta === 'object') {
|
|
1774
|
+
c.delta = { ...c.delta };
|
|
1775
|
+
if (typeof c.delta.content === 'string') {
|
|
1776
|
+
c.delta.content = detok.feed(c.delta.content);
|
|
1777
|
+
}
|
|
1778
|
+
if (typeof c.delta.reasoning_content === 'string') {
|
|
1779
|
+
c.delta.reasoning_content = detok.feed(c.delta.reasoning_content);
|
|
1780
|
+
}
|
|
1781
|
+
if (typeof c.delta.text === 'string') {
|
|
1782
|
+
c.delta.text = detok.feed(c.delta.text);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (typeof c.text === 'string') {
|
|
1786
|
+
c.text = detok.feed(c.text);
|
|
1787
|
+
}
|
|
1788
|
+
return c;
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
if (parsed.delta && typeof parsed.delta === 'object') {
|
|
1792
|
+
parsed.delta = { ...parsed.delta };
|
|
1793
|
+
if (typeof parsed.delta.text === 'string') {
|
|
1794
|
+
parsed.delta.text = detok.feed(parsed.delta.text);
|
|
1795
|
+
}
|
|
1796
|
+
if (typeof parsed.delta.partial_json === 'string') {
|
|
1797
|
+
parsed.delta.partial_json = detok.feed(parsed.delta.partial_json);
|
|
1798
|
+
}
|
|
1799
|
+
} else if (typeof parsed.delta === 'string') {
|
|
1800
|
+
parsed.delta = detok.feed(parsed.delta);
|
|
1801
|
+
}
|
|
1802
|
+
if (Array.isArray(parsed.candidates)) {
|
|
1803
|
+
parsed.candidates = parsed.candidates.map(cand => {
|
|
1804
|
+
if (cand && cand.content && Array.isArray(cand.content.parts)) {
|
|
1805
|
+
return {
|
|
1806
|
+
...cand,
|
|
1807
|
+
content: {
|
|
1808
|
+
...cand.content,
|
|
1809
|
+
parts: cand.content.parts.map(p =>
|
|
1810
|
+
(p && typeof p.text === 'string') ? { ...p, text: detok.feed(p.text) } : p
|
|
1811
|
+
)
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
return cand;
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (typeof parsed.response === 'string') {
|
|
1819
|
+
parsed.response = detok.feed(parsed.response);
|
|
1820
|
+
}
|
|
1821
|
+
return parsed;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const privacyFetch = async function(input, init = {}) {
|
|
1825
|
+
if (typeof baseFetch !== 'function') {
|
|
1826
|
+
throw new Error('[PrivacyScrubber] No global fetch implementation found; pass { fetch } to createPrivacyFetch.');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1830
|
+
let modifiedInit = init ? { ...init } : {};
|
|
1831
|
+
|
|
1832
|
+
if (sanitizeInputs && modifiedInit && typeof modifiedInit.body === 'string') {
|
|
1833
|
+
const trimmed = modifiedInit.body.trim();
|
|
1834
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
1835
|
+
try {
|
|
1836
|
+
const parsed = JSON.parse(modifiedInit.body);
|
|
1837
|
+
const sanitizedBody = sanitizeJsonNode(parsed, sessionTokenMap);
|
|
1838
|
+
modifiedInit.body = JSON.stringify(sanitizedBody);
|
|
1839
|
+
if (modifiedInit.headers) {
|
|
1840
|
+
if (typeof Headers !== 'undefined' && modifiedInit.headers instanceof Headers) {
|
|
1841
|
+
const h = new Headers(modifiedInit.headers);
|
|
1842
|
+
h.delete('content-length');
|
|
1843
|
+
modifiedInit.headers = h;
|
|
1844
|
+
} else if (typeof modifiedInit.headers === 'object' && !Array.isArray(modifiedInit.headers)) {
|
|
1845
|
+
const h = { ...modifiedInit.headers };
|
|
1846
|
+
delete h['content-length'];
|
|
1847
|
+
delete h['Content-Length'];
|
|
1848
|
+
modifiedInit.headers = h;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (typeof options.onSanitized === 'function') {
|
|
1852
|
+
try {
|
|
1853
|
+
options.onSanitized({
|
|
1854
|
+
url: typeof input === 'string' ? input : (input && input.url) || '',
|
|
1855
|
+
tokenMap: sessionTokenMap,
|
|
1856
|
+
tokenCount: Object.keys(sessionTokenMap).length
|
|
1857
|
+
});
|
|
1858
|
+
} catch (_) {}
|
|
1859
|
+
}
|
|
1860
|
+
} catch (_) {}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const response = await baseFetch(input, modifiedInit);
|
|
1865
|
+
if (!sanitizeOutputs || !response || Object.keys(sessionTokenMap).length === 0) {
|
|
1866
|
+
return response;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const contentType = (response.headers && typeof response.headers.get === 'function')
|
|
1870
|
+
? (response.headers.get('content-type') || '').toLowerCase()
|
|
1871
|
+
: '';
|
|
1872
|
+
|
|
1873
|
+
if (contentType.includes('text/event-stream') && response.body && typeof response.body.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
1874
|
+
const detok = new StreamingDetokenizer(sessionTokenMap, options.streamingOptions);
|
|
1875
|
+
const decoder = new TextDecoder();
|
|
1876
|
+
const encoder = new TextEncoder();
|
|
1877
|
+
let lineBuffer = '';
|
|
1878
|
+
|
|
1879
|
+
const processSseLine = (line) => {
|
|
1880
|
+
if (!line.startsWith('data:')) return line;
|
|
1881
|
+
const dataPayload = line.slice(5).trim();
|
|
1882
|
+
if (!dataPayload || dataPayload === '[DONE]') {
|
|
1883
|
+
const rem = detok.flush();
|
|
1884
|
+
return rem ? `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: rem } }] })}\n\n${line}` : line;
|
|
1885
|
+
}
|
|
1886
|
+
if (dataPayload.startsWith('{')) {
|
|
1887
|
+
try {
|
|
1888
|
+
const parsed = JSON.parse(dataPayload);
|
|
1889
|
+
const restoredObj = detokenizeSseJsonPayload(parsed, detok);
|
|
1890
|
+
return `data: ${JSON.stringify(restoredObj)}`;
|
|
1891
|
+
} catch (_) {
|
|
1892
|
+
return `data: ${detok.feed(dataPayload)}`;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return `data: ${detok.feed(dataPayload)}`;
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
const transformedBody = response.body.pipeThrough(new TransformStream({
|
|
1899
|
+
transform(chunk, controller) {
|
|
1900
|
+
const str = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
1901
|
+
lineBuffer += str;
|
|
1902
|
+
const lines = lineBuffer.split(/\r?\n/);
|
|
1903
|
+
lineBuffer = lines.pop() || '';
|
|
1904
|
+
if (lines.length > 0) {
|
|
1905
|
+
const outLines = lines.map(processSseLine).join('\n') + '\n';
|
|
1906
|
+
controller.enqueue(encoder.encode(outLines));
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
flush(controller) {
|
|
1910
|
+
let finalOut = '';
|
|
1911
|
+
if (lineBuffer) {
|
|
1912
|
+
finalOut += processSseLine(lineBuffer) + '\n';
|
|
1913
|
+
lineBuffer = '';
|
|
1914
|
+
}
|
|
1915
|
+
const rem = detok.flush();
|
|
1916
|
+
if (rem) {
|
|
1917
|
+
finalOut += `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: rem } }] })}\n\n`;
|
|
1918
|
+
}
|
|
1919
|
+
if (finalOut) {
|
|
1920
|
+
controller.enqueue(encoder.encode(finalOut));
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}));
|
|
1924
|
+
|
|
1925
|
+
if (typeof Response !== 'undefined') {
|
|
1926
|
+
return new Response(transformedBody, {
|
|
1927
|
+
status: response.status,
|
|
1928
|
+
statusText: response.statusText,
|
|
1929
|
+
headers: response.headers
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return { ...response, body: transformedBody };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (contentType.includes('application/json') || typeof response.json === 'function') {
|
|
1936
|
+
try {
|
|
1937
|
+
const rawText = typeof response.text === 'function'
|
|
1938
|
+
? await response.text()
|
|
1939
|
+
: JSON.stringify(await response.json());
|
|
1940
|
+
const parsed = JSON.parse(rawText);
|
|
1941
|
+
const restoredObj = restoreObject(parsed, sessionTokenMap);
|
|
1942
|
+
const restoredText = JSON.stringify(restoredObj);
|
|
1943
|
+
if (typeof Response !== 'undefined') {
|
|
1944
|
+
const headers = new Headers(response.headers || {});
|
|
1945
|
+
headers.delete('content-length');
|
|
1946
|
+
return new Response(restoredText, {
|
|
1947
|
+
status: response.status,
|
|
1948
|
+
statusText: response.statusText,
|
|
1949
|
+
headers
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
...response,
|
|
1954
|
+
text: async () => restoredText,
|
|
1955
|
+
json: async () => restoredObj
|
|
1956
|
+
};
|
|
1957
|
+
} catch (_) {
|
|
1958
|
+
return response;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
return response;
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
privacyFetch.engine = engine;
|
|
1966
|
+
return privacyFetch;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Creates OpenAI Agents SDK (@openai/agents) compatible InputGuardrail and OutputGuardrail objects.
|
|
1971
|
+
*
|
|
1972
|
+
* Supports two operating modes:
|
|
1973
|
+
* 1. Sanitization & Telemetry Mode (default): Scrubs PII/secrets into engine.sessionMap and attaches
|
|
1974
|
+
* CISO compliance telemetry to `outputInfo` while allowing execution (`tripwireTriggered: false`).
|
|
1975
|
+
* 2. Strict Tripwire Mode (`tripwireOnCritical: true` or `tripwireOnAnyPii: true`): Triggers the
|
|
1976
|
+
* OpenAI Agents SDK guardrail tripwire (`tripwireTriggered: true`) to halt agent execution when
|
|
1977
|
+
* critical secrets/PII or any PII are detected.
|
|
1978
|
+
*
|
|
1979
|
+
* @param {object} [options={}] - Configuration options.
|
|
1980
|
+
* @returns {object} `{ inputGuardrail, outputGuardrail, inputGuardrails, outputGuardrails, engine }`
|
|
1981
|
+
*/
|
|
1982
|
+
export function createOpenAIAgentGuardrails(options = {}) {
|
|
1983
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1984
|
+
const tripwireOnCritical = options.tripwireOnCritical === true;
|
|
1985
|
+
const tripwireOnAnyPii = options.tripwireOnAnyPii === true;
|
|
1986
|
+
|
|
1987
|
+
function extractAndScrubAgentInput(input, sessionTokenMap) {
|
|
1988
|
+
if (typeof input === 'string') {
|
|
1989
|
+
const res = engine.sanitize(input, { existingSessionMap: sessionTokenMap });
|
|
1990
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1991
|
+
return { scrubbedInput: res.scrubbedText, count: res.count };
|
|
1992
|
+
}
|
|
1993
|
+
if (Array.isArray(input)) {
|
|
1994
|
+
let totalCount = 0;
|
|
1995
|
+
const scrubbedList = input.map(item => {
|
|
1996
|
+
if (typeof item === 'string') {
|
|
1997
|
+
const r = engine.sanitize(item, { existingSessionMap: sessionTokenMap });
|
|
1998
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
1999
|
+
totalCount += r.count;
|
|
2000
|
+
return r.scrubbedText;
|
|
2001
|
+
}
|
|
2002
|
+
if (item && typeof item === 'object') {
|
|
2003
|
+
const copy = { ...item };
|
|
2004
|
+
if (typeof copy.content === 'string') {
|
|
2005
|
+
const r = engine.sanitize(copy.content, { existingSessionMap: sessionTokenMap });
|
|
2006
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
2007
|
+
totalCount += r.count;
|
|
2008
|
+
copy.content = r.scrubbedText;
|
|
2009
|
+
if (options.mutateInPlace !== false) item.content = r.scrubbedText;
|
|
2010
|
+
} else if (Array.isArray(copy.content)) {
|
|
2011
|
+
copy.content = copy.content.map(part => {
|
|
2012
|
+
if (part && typeof part.text === 'string') {
|
|
2013
|
+
const r = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap });
|
|
2014
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
2015
|
+
totalCount += r.count;
|
|
2016
|
+
if (options.mutateInPlace !== false) part.text = r.scrubbedText;
|
|
2017
|
+
return { ...part, text: r.scrubbedText };
|
|
2018
|
+
}
|
|
2019
|
+
return part;
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
return copy;
|
|
2023
|
+
}
|
|
2024
|
+
return item;
|
|
2025
|
+
});
|
|
2026
|
+
return { scrubbedInput: scrubbedList, count: totalCount };
|
|
2027
|
+
}
|
|
2028
|
+
return { scrubbedInput: input, count: 0 };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const inputGuardrail = {
|
|
2032
|
+
name: options.inputGuardrailName || 'PrivacyScrubber ZTDS Input Guardrail',
|
|
2033
|
+
async execute({ input, context } = {}) {
|
|
2034
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
2035
|
+
const { scrubbedInput, count } = extractAndScrubAgentInput(input, sessionTokenMap);
|
|
2036
|
+
Object.assign(engine.sessionMap, sessionTokenMap);
|
|
2037
|
+
|
|
2038
|
+
if (context && typeof context === 'object') {
|
|
2039
|
+
context.ztdsTokenMap = engine.sessionMap;
|
|
2040
|
+
context.ztdsScrubbedInput = scrubbedInput;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const telemetry = buildAuditTelemetry(sessionTokenMap);
|
|
2044
|
+
const isCritical = telemetry.riskLevel && telemetry.riskLevel.includes('CRITICAL');
|
|
2045
|
+
const tripwireTriggered = Boolean(
|
|
2046
|
+
(tripwireOnAnyPii && count > 0) ||
|
|
2047
|
+
(tripwireOnCritical && count > 0 && isCritical)
|
|
2048
|
+
);
|
|
2049
|
+
|
|
2050
|
+
return {
|
|
2051
|
+
tripwireTriggered,
|
|
2052
|
+
outputInfo: {
|
|
2053
|
+
scrubbedInput,
|
|
2054
|
+
tokenMap: { ...sessionTokenMap },
|
|
2055
|
+
tokenCount: count,
|
|
2056
|
+
riskLevel: telemetry.riskLevel,
|
|
2057
|
+
frameworks: telemetry.frameworksList,
|
|
2058
|
+
telemetry
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
const outputGuardrail = {
|
|
2065
|
+
name: options.outputGuardrailName || 'PrivacyScrubber ZTDS Output Guardrail',
|
|
2066
|
+
async execute({ agentOutput, context } = {}) {
|
|
2067
|
+
const activeMap = (context && context.ztdsTokenMap) || engine.sessionMap || {};
|
|
2068
|
+
let restoredOutput = agentOutput;
|
|
2069
|
+
let restoredCount = 0;
|
|
2070
|
+
|
|
2071
|
+
if (typeof agentOutput === 'string') {
|
|
2072
|
+
const res = engine.restore(agentOutput, activeMap);
|
|
2073
|
+
restoredOutput = res.restoredText;
|
|
2074
|
+
restoredCount = res.restoredCount;
|
|
2075
|
+
} else if (agentOutput && typeof agentOutput === 'object') {
|
|
2076
|
+
restoredOutput = restoreObject(agentOutput, activeMap);
|
|
2077
|
+
if (options.mutateInPlace !== false) {
|
|
2078
|
+
Object.assign(agentOutput, restoredOutput);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (context && typeof context === 'object') {
|
|
2083
|
+
context.ztdsRestoredOutput = restoredOutput;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
let leakDetected = false;
|
|
2087
|
+
if (options.tripwireOnOutputLeak === true && typeof restoredOutput === 'string') {
|
|
2088
|
+
const check = engine.sanitize(restoredOutput, { persistSession: false });
|
|
2089
|
+
leakDetected = check.count > 0;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
return {
|
|
2093
|
+
tripwireTriggered: leakDetected,
|
|
2094
|
+
outputInfo: {
|
|
2095
|
+
restoredOutput,
|
|
2096
|
+
restoredCount
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
return {
|
|
2103
|
+
inputGuardrail,
|
|
2104
|
+
outputGuardrail,
|
|
2105
|
+
inputGuardrails: [inputGuardrail],
|
|
2106
|
+
outputGuardrails: [outputGuardrail],
|
|
2107
|
+
engine
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1122
2111
|
/**
|
|
1123
2112
|
* Framework middleware helper for LangChain / LlamaIndex pipelines.
|
|
1124
2113
|
*/
|
|
@@ -1758,9 +2747,11 @@ export class StreamingDetokenizer {
|
|
|
1758
2747
|
*/
|
|
1759
2748
|
async* wrapStream(asyncIterable) {
|
|
1760
2749
|
let lastOpenAIChunk = null;
|
|
2750
|
+
let lastResponsesDeltaChunk = null;
|
|
2751
|
+
let lastAnthropicDeltaChunk = null;
|
|
1761
2752
|
for await (const chunk of asyncIterable) {
|
|
1762
2753
|
if (chunk === null || chunk === undefined) continue;
|
|
1763
|
-
// Handle OpenAI chunk object
|
|
2754
|
+
// Handle OpenAI chat.completions chunk object
|
|
1764
2755
|
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
1765
2756
|
lastOpenAIChunk = chunk;
|
|
1766
2757
|
const delta = chunk.choices[0].delta.content;
|
|
@@ -1778,6 +2769,55 @@ export class StreamingDetokenizer {
|
|
|
1778
2769
|
continue;
|
|
1779
2770
|
}
|
|
1780
2771
|
|
|
2772
|
+
// Handle OpenAI Responses API streaming events (response.output_text.delta / done)
|
|
2773
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && chunk.type.startsWith('response.')) {
|
|
2774
|
+
if (chunk.type === 'response.output_text.delta' && typeof chunk.delta === 'string') {
|
|
2775
|
+
lastResponsesDeltaChunk = chunk;
|
|
2776
|
+
const restored = this.feed(chunk.delta);
|
|
2777
|
+
if (restored) {
|
|
2778
|
+
yield { ...chunk, delta: restored };
|
|
2779
|
+
}
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
if (chunk.type === 'response.output_text.done' && typeof chunk.text === 'string') {
|
|
2783
|
+
const remaining = this.flush();
|
|
2784
|
+
if (remaining && lastResponsesDeltaChunk) {
|
|
2785
|
+
yield { ...lastResponsesDeltaChunk, delta: remaining };
|
|
2786
|
+
}
|
|
2787
|
+
yield { ...chunk, text: this.replacer(chunk.text) };
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
yield chunk;
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// Handle Anthropic messages.create({ stream: true }) events (content_block_delta / content_block_stop)
|
|
2795
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && (
|
|
2796
|
+
chunk.type === 'content_block_delta' ||
|
|
2797
|
+
chunk.type === 'content_block_start' ||
|
|
2798
|
+
chunk.type === 'content_block_stop' ||
|
|
2799
|
+
chunk.type === 'message_start' ||
|
|
2800
|
+
chunk.type === 'message_delta' ||
|
|
2801
|
+
chunk.type === 'message_stop'
|
|
2802
|
+
)) {
|
|
2803
|
+
if (chunk.type === 'content_block_delta' && chunk.delta && chunk.delta.type === 'text_delta' && typeof chunk.delta.text === 'string') {
|
|
2804
|
+
lastAnthropicDeltaChunk = chunk;
|
|
2805
|
+
const restored = this.feed(chunk.delta.text);
|
|
2806
|
+
if (restored) {
|
|
2807
|
+
yield { ...chunk, delta: { ...chunk.delta, text: restored } };
|
|
2808
|
+
}
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
if ((chunk.type === 'content_block_stop' || chunk.type === 'message_stop') && this.pendingBuffer) {
|
|
2812
|
+
const remaining = this.flush();
|
|
2813
|
+
if (remaining && lastAnthropicDeltaChunk) {
|
|
2814
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: remaining } };
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
yield chunk;
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
1781
2821
|
let text = '';
|
|
1782
2822
|
if (typeof chunk === 'string') {
|
|
1783
2823
|
text = chunk;
|
|
@@ -1802,6 +2842,10 @@ export class StreamingDetokenizer {
|
|
|
1802
2842
|
idx === 0 ? { ...c, delta: { ...c.delta, content: finalChunk } } : c
|
|
1803
2843
|
);
|
|
1804
2844
|
yield { ...lastOpenAIChunk, choices: clonedChoices };
|
|
2845
|
+
} else if (lastResponsesDeltaChunk) {
|
|
2846
|
+
yield { ...lastResponsesDeltaChunk, delta: finalChunk };
|
|
2847
|
+
} else if (lastAnthropicDeltaChunk) {
|
|
2848
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: finalChunk } };
|
|
1805
2849
|
} else {
|
|
1806
2850
|
yield finalChunk;
|
|
1807
2851
|
}
|
|
@@ -1963,7 +3007,7 @@ export function formatCefEvent(eventData = {}) {
|
|
|
1963
3007
|
const {
|
|
1964
3008
|
vendor = 'PrivacyScrubber',
|
|
1965
3009
|
product = 'ZTDS',
|
|
1966
|
-
version =
|
|
3010
|
+
version = SDK_VERSION,
|
|
1967
3011
|
eventClassId = 'PII_REDACTION',
|
|
1968
3012
|
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1969
3013
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2032,7 +3076,7 @@ export function formatSyslogEvent(eventData = {}) {
|
|
|
2032
3076
|
export function formatJsonlEvent(eventData = {}) {
|
|
2033
3077
|
const {
|
|
2034
3078
|
timestamp = new Date().toISOString(),
|
|
2035
|
-
version =
|
|
3079
|
+
version = SDK_VERSION,
|
|
2036
3080
|
protocol = 'ZTDS',
|
|
2037
3081
|
event = 'sanitization',
|
|
2038
3082
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2080,12 +3124,22 @@ export function createAuditLogger(options = {}) {
|
|
|
2080
3124
|
const buffer = [];
|
|
2081
3125
|
|
|
2082
3126
|
if (typeof destination === 'string' && destination.trim()) {
|
|
3127
|
+
if (destination.includes('\0')) {
|
|
3128
|
+
throw new Error('[PrivacyScrubber AuditLogger] Security violation: null byte in destination path.');
|
|
3129
|
+
}
|
|
3130
|
+
const resolvedDest = path.resolve(destination.trim());
|
|
3131
|
+
if (fs.existsSync(resolvedDest)) {
|
|
3132
|
+
const st = fs.lstatSync(resolvedDest);
|
|
3133
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
3134
|
+
throw new Error('[PrivacyScrubber AuditLogger] Security violation: destination must be a regular non-symlink file.');
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
2083
3137
|
try {
|
|
2084
|
-
const dir = path.dirname(
|
|
3138
|
+
const dir = path.dirname(resolvedDest);
|
|
2085
3139
|
if (!fs.existsSync(dir)) {
|
|
2086
3140
|
fs.mkdirSync(dir, { recursive: true });
|
|
2087
3141
|
}
|
|
2088
|
-
fileHandle =
|
|
3142
|
+
fileHandle = resolvedDest;
|
|
2089
3143
|
} catch {}
|
|
2090
3144
|
} else if (destination && typeof destination.write === 'function') {
|
|
2091
3145
|
streamHandle = destination;
|
|
@@ -2115,6 +3169,12 @@ export function createAuditLogger(options = {}) {
|
|
|
2115
3169
|
buffer.push(line);
|
|
2116
3170
|
if (fileHandle) {
|
|
2117
3171
|
try {
|
|
3172
|
+
if (fs.existsSync(fileHandle)) {
|
|
3173
|
+
const st = fs.lstatSync(fileHandle);
|
|
3174
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
3175
|
+
throw new Error('Target destination became a symlink or non-regular file.');
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
2118
3178
|
fs.appendFileSync(fileHandle, line + '\n', 'utf8');
|
|
2119
3179
|
} catch (e) {
|
|
2120
3180
|
console.error('[PrivacyScrubber AuditLogger] File write error:', e);
|
|
@@ -2418,11 +3478,41 @@ export function applyPatch(filePath, content, options = {}) {
|
|
|
2418
3478
|
const {
|
|
2419
3479
|
tokenMap = null,
|
|
2420
3480
|
engine = null,
|
|
2421
|
-
createBackup = true
|
|
3481
|
+
createBackup = true,
|
|
3482
|
+
cwd = process.cwd(),
|
|
3483
|
+
allowOutsideWorkspace = false
|
|
2422
3484
|
} = options;
|
|
2423
3485
|
|
|
3486
|
+
if (filePath.includes('\0')) {
|
|
3487
|
+
throw new Error('[applyPatch] Security policy violation — null byte in filePath blocked.');
|
|
3488
|
+
}
|
|
3489
|
+
|
|
2424
3490
|
const activeTokenMap = tokenMap || (engine && engine.sessionMap) || {};
|
|
2425
|
-
const
|
|
3491
|
+
const workspaceRoot = fs.existsSync(cwd) ? fs.realpathSync(cwd) : path.resolve(cwd);
|
|
3492
|
+
const tmpRoot = fs.existsSync(os.tmpdir()) ? fs.realpathSync(os.tmpdir()) : path.resolve(os.tmpdir());
|
|
3493
|
+
const resolvedPath = path.resolve(workspaceRoot, filePath);
|
|
3494
|
+
|
|
3495
|
+
let checkCursor = resolvedPath;
|
|
3496
|
+
while (!fs.existsSync(checkCursor)) {
|
|
3497
|
+
const parentCursor = path.dirname(checkCursor);
|
|
3498
|
+
if (parentCursor === checkCursor) break;
|
|
3499
|
+
checkCursor = parentCursor;
|
|
3500
|
+
}
|
|
3501
|
+
const realExistingCursor = fs.existsSync(checkCursor) ? fs.realpathSync(checkCursor) : checkCursor;
|
|
3502
|
+
const suffixFromCursor = path.relative(checkCursor, resolvedPath);
|
|
3503
|
+
const canonicalTarget = path.resolve(realExistingCursor, suffixFromCursor);
|
|
3504
|
+
|
|
3505
|
+
if (!allowOutsideWorkspace) {
|
|
3506
|
+
const hasTraversalSegments = /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(String(filePath));
|
|
3507
|
+
const relToWorkspace = path.relative(workspaceRoot, canonicalTarget);
|
|
3508
|
+
const isInsideWorkspace = !relToWorkspace.startsWith('..') && !path.isAbsolute(relToWorkspace);
|
|
3509
|
+
const relToTmp = path.relative(tmpRoot, canonicalTarget);
|
|
3510
|
+
const isInsideTmp = path.isAbsolute(filePath) && !hasTraversalSegments && !relToTmp.startsWith('..') && !path.isAbsolute(relToTmp);
|
|
3511
|
+
if (!isInsideWorkspace && !isInsideTmp) {
|
|
3512
|
+
throw new Error(`[applyPatch] Security policy violation — target 'filePath' resolves outside the active workspace directory (path traversal blocked).`);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
|
|
2426
3516
|
const restored = unscrub(content, activeTokenMap);
|
|
2427
3517
|
|
|
2428
3518
|
let backupCreated = false;
|
|
@@ -2679,7 +3769,9 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
2679
3769
|
const safeFilePath = resolveSafeToolPath(filePath, 'guard_apply_patch', 'filePath');
|
|
2680
3770
|
const patchResult = applyPatch(safeFilePath, content, {
|
|
2681
3771
|
tokenMap: engine.sessionMap,
|
|
2682
|
-
createBackup
|
|
3772
|
+
createBackup,
|
|
3773
|
+
cwd: baseCwd,
|
|
3774
|
+
allowOutsideWorkspace
|
|
2683
3775
|
});
|
|
2684
3776
|
|
|
2685
3777
|
const responseText = `[Zero-Trust Agentic Guard: Patch Applied]\n` +
|
|
@@ -2795,6 +3887,7 @@ import {
|
|
|
2795
3887
|
} from './vision.js';
|
|
2796
3888
|
|
|
2797
3889
|
export default {
|
|
3890
|
+
SDK_VERSION,
|
|
2798
3891
|
sanitize,
|
|
2799
3892
|
sanitizeObject,
|
|
2800
3893
|
scrub,
|
|
@@ -2821,8 +3914,13 @@ export default {
|
|
|
2821
3914
|
PrivacyScrubberEngine,
|
|
2822
3915
|
PrivacyScrubberLicenseError,
|
|
2823
3916
|
wrapOpenAI,
|
|
3917
|
+
wrapAnthropic,
|
|
3918
|
+
wrapGemini,
|
|
3919
|
+
wrapGoogleGenAI,
|
|
2824
3920
|
wrapAiModel,
|
|
2825
3921
|
wrapAiSdk,
|
|
3922
|
+
createPrivacyFetch,
|
|
3923
|
+
createOpenAIAgentGuardrails,
|
|
2826
3924
|
createLangChainTransform,
|
|
2827
3925
|
createDocumentTransformer,
|
|
2828
3926
|
createLangChainDocumentTransformer,
|