@intflows/genkit-guard 0.0.8-alpha.1 → 0.0.9
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/dist/middleware/middleware.d.ts +300 -1
- package/dist/middleware/middleware.js +142 -103
- package/package.json +5 -1
- package/dist/src/core/types.d.ts +0 -18
- package/dist/src/core/types.js +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -22
- package/dist/src/intent/intentAnalyzer.d.ts +0 -6
- package/dist/src/intent/intentAnalyzer.js +0 -80
- package/dist/src/middleware/middleware.d.ts +0 -1
- package/dist/src/middleware/middleware.js +0 -120
- package/dist/src/pii/detector.d.ts +0 -10
- package/dist/src/pii/detector.js +0 -47
- package/dist/src/pii/tokenizer.d.ts +0 -19
- package/dist/src/pii/tokenizer.js +0 -32
- package/dist/src/util/singleton.d.ts +0 -14
- package/dist/src/util/singleton.js +0 -60
|
@@ -1 +1,300 @@
|
|
|
1
|
-
|
|
1
|
+
import { z } from 'genkit';
|
|
2
|
+
declare const guardConfigSchema: z.ZodObject<{
|
|
3
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
4
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
5
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
6
|
+
semantic: z.ZodObject<{
|
|
7
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
intents: Record<string, string>;
|
|
11
|
+
threshold?: number | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
intents: Record<string, string>;
|
|
14
|
+
threshold?: number | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
semantic: {
|
|
18
|
+
intents: Record<string, string>;
|
|
19
|
+
threshold?: number | undefined;
|
|
20
|
+
};
|
|
21
|
+
mode?: string | undefined;
|
|
22
|
+
allowedIntent?: string | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
semantic: {
|
|
25
|
+
intents: Record<string, string>;
|
|
26
|
+
threshold?: number | undefined;
|
|
27
|
+
};
|
|
28
|
+
mode?: string | undefined;
|
|
29
|
+
allowedIntent?: string | undefined;
|
|
30
|
+
}>>;
|
|
31
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
model: z.ZodOptional<z.ZodString>;
|
|
34
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
mode?: "ner" | "classifier" | undefined;
|
|
37
|
+
reversible?: boolean | undefined;
|
|
38
|
+
model?: string | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
mode?: "ner" | "classifier" | undefined;
|
|
41
|
+
reversible?: boolean | undefined;
|
|
42
|
+
model?: string | undefined;
|
|
43
|
+
}>>;
|
|
44
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
extractor?: string | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
extractor?: string | undefined;
|
|
50
|
+
}>>;
|
|
51
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
52
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
54
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
55
|
+
semantic: z.ZodObject<{
|
|
56
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
intents: Record<string, string>;
|
|
60
|
+
threshold?: number | undefined;
|
|
61
|
+
}, {
|
|
62
|
+
intents: Record<string, string>;
|
|
63
|
+
threshold?: number | undefined;
|
|
64
|
+
}>;
|
|
65
|
+
}, "strip", z.ZodTypeAny, {
|
|
66
|
+
semantic: {
|
|
67
|
+
intents: Record<string, string>;
|
|
68
|
+
threshold?: number | undefined;
|
|
69
|
+
};
|
|
70
|
+
mode?: string | undefined;
|
|
71
|
+
allowedIntent?: string | undefined;
|
|
72
|
+
}, {
|
|
73
|
+
semantic: {
|
|
74
|
+
intents: Record<string, string>;
|
|
75
|
+
threshold?: number | undefined;
|
|
76
|
+
};
|
|
77
|
+
mode?: string | undefined;
|
|
78
|
+
allowedIntent?: string | undefined;
|
|
79
|
+
}>>;
|
|
80
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
82
|
+
model: z.ZodOptional<z.ZodString>;
|
|
83
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
84
|
+
}, "strip", z.ZodTypeAny, {
|
|
85
|
+
mode?: "ner" | "classifier" | undefined;
|
|
86
|
+
reversible?: boolean | undefined;
|
|
87
|
+
model?: string | undefined;
|
|
88
|
+
}, {
|
|
89
|
+
mode?: "ner" | "classifier" | undefined;
|
|
90
|
+
reversible?: boolean | undefined;
|
|
91
|
+
model?: string | undefined;
|
|
92
|
+
}>>;
|
|
93
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
94
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
95
|
+
}, "strip", z.ZodTypeAny, {
|
|
96
|
+
extractor?: string | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
extractor?: string | undefined;
|
|
99
|
+
}>>;
|
|
100
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
101
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
103
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
104
|
+
semantic: z.ZodObject<{
|
|
105
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
107
|
+
}, "strip", z.ZodTypeAny, {
|
|
108
|
+
intents: Record<string, string>;
|
|
109
|
+
threshold?: number | undefined;
|
|
110
|
+
}, {
|
|
111
|
+
intents: Record<string, string>;
|
|
112
|
+
threshold?: number | undefined;
|
|
113
|
+
}>;
|
|
114
|
+
}, "strip", z.ZodTypeAny, {
|
|
115
|
+
semantic: {
|
|
116
|
+
intents: Record<string, string>;
|
|
117
|
+
threshold?: number | undefined;
|
|
118
|
+
};
|
|
119
|
+
mode?: string | undefined;
|
|
120
|
+
allowedIntent?: string | undefined;
|
|
121
|
+
}, {
|
|
122
|
+
semantic: {
|
|
123
|
+
intents: Record<string, string>;
|
|
124
|
+
threshold?: number | undefined;
|
|
125
|
+
};
|
|
126
|
+
mode?: string | undefined;
|
|
127
|
+
allowedIntent?: string | undefined;
|
|
128
|
+
}>>;
|
|
129
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
130
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
131
|
+
model: z.ZodOptional<z.ZodString>;
|
|
132
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
133
|
+
}, "strip", z.ZodTypeAny, {
|
|
134
|
+
mode?: "ner" | "classifier" | undefined;
|
|
135
|
+
reversible?: boolean | undefined;
|
|
136
|
+
model?: string | undefined;
|
|
137
|
+
}, {
|
|
138
|
+
mode?: "ner" | "classifier" | undefined;
|
|
139
|
+
reversible?: boolean | undefined;
|
|
140
|
+
model?: string | undefined;
|
|
141
|
+
}>>;
|
|
142
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
143
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
144
|
+
}, "strip", z.ZodTypeAny, {
|
|
145
|
+
extractor?: string | undefined;
|
|
146
|
+
}, {
|
|
147
|
+
extractor?: string | undefined;
|
|
148
|
+
}>>;
|
|
149
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
150
|
+
export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
|
|
151
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
152
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
153
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
154
|
+
semantic: z.ZodObject<{
|
|
155
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
157
|
+
}, "strip", z.ZodTypeAny, {
|
|
158
|
+
intents: Record<string, string>;
|
|
159
|
+
threshold?: number | undefined;
|
|
160
|
+
}, {
|
|
161
|
+
intents: Record<string, string>;
|
|
162
|
+
threshold?: number | undefined;
|
|
163
|
+
}>;
|
|
164
|
+
}, "strip", z.ZodTypeAny, {
|
|
165
|
+
semantic: {
|
|
166
|
+
intents: Record<string, string>;
|
|
167
|
+
threshold?: number | undefined;
|
|
168
|
+
};
|
|
169
|
+
mode?: string | undefined;
|
|
170
|
+
allowedIntent?: string | undefined;
|
|
171
|
+
}, {
|
|
172
|
+
semantic: {
|
|
173
|
+
intents: Record<string, string>;
|
|
174
|
+
threshold?: number | undefined;
|
|
175
|
+
};
|
|
176
|
+
mode?: string | undefined;
|
|
177
|
+
allowedIntent?: string | undefined;
|
|
178
|
+
}>>;
|
|
179
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
180
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
181
|
+
model: z.ZodOptional<z.ZodString>;
|
|
182
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
183
|
+
}, "strip", z.ZodTypeAny, {
|
|
184
|
+
mode?: "ner" | "classifier" | undefined;
|
|
185
|
+
reversible?: boolean | undefined;
|
|
186
|
+
model?: string | undefined;
|
|
187
|
+
}, {
|
|
188
|
+
mode?: "ner" | "classifier" | undefined;
|
|
189
|
+
reversible?: boolean | undefined;
|
|
190
|
+
model?: string | undefined;
|
|
191
|
+
}>>;
|
|
192
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
193
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
194
|
+
}, "strip", z.ZodTypeAny, {
|
|
195
|
+
extractor?: string | undefined;
|
|
196
|
+
}, {
|
|
197
|
+
extractor?: string | undefined;
|
|
198
|
+
}>>;
|
|
199
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
200
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
201
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
202
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
203
|
+
semantic: z.ZodObject<{
|
|
204
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
205
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
206
|
+
}, "strip", z.ZodTypeAny, {
|
|
207
|
+
intents: Record<string, string>;
|
|
208
|
+
threshold?: number | undefined;
|
|
209
|
+
}, {
|
|
210
|
+
intents: Record<string, string>;
|
|
211
|
+
threshold?: number | undefined;
|
|
212
|
+
}>;
|
|
213
|
+
}, "strip", z.ZodTypeAny, {
|
|
214
|
+
semantic: {
|
|
215
|
+
intents: Record<string, string>;
|
|
216
|
+
threshold?: number | undefined;
|
|
217
|
+
};
|
|
218
|
+
mode?: string | undefined;
|
|
219
|
+
allowedIntent?: string | undefined;
|
|
220
|
+
}, {
|
|
221
|
+
semantic: {
|
|
222
|
+
intents: Record<string, string>;
|
|
223
|
+
threshold?: number | undefined;
|
|
224
|
+
};
|
|
225
|
+
mode?: string | undefined;
|
|
226
|
+
allowedIntent?: string | undefined;
|
|
227
|
+
}>>;
|
|
228
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
229
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
230
|
+
model: z.ZodOptional<z.ZodString>;
|
|
231
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
232
|
+
}, "strip", z.ZodTypeAny, {
|
|
233
|
+
mode?: "ner" | "classifier" | undefined;
|
|
234
|
+
reversible?: boolean | undefined;
|
|
235
|
+
model?: string | undefined;
|
|
236
|
+
}, {
|
|
237
|
+
mode?: "ner" | "classifier" | undefined;
|
|
238
|
+
reversible?: boolean | undefined;
|
|
239
|
+
model?: string | undefined;
|
|
240
|
+
}>>;
|
|
241
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
242
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
243
|
+
}, "strip", z.ZodTypeAny, {
|
|
244
|
+
extractor?: string | undefined;
|
|
245
|
+
}, {
|
|
246
|
+
extractor?: string | undefined;
|
|
247
|
+
}>>;
|
|
248
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
249
|
+
intent: z.ZodOptional<z.ZodObject<{
|
|
250
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
251
|
+
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
252
|
+
semantic: z.ZodObject<{
|
|
253
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
254
|
+
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
255
|
+
}, "strip", z.ZodTypeAny, {
|
|
256
|
+
intents: Record<string, string>;
|
|
257
|
+
threshold?: number | undefined;
|
|
258
|
+
}, {
|
|
259
|
+
intents: Record<string, string>;
|
|
260
|
+
threshold?: number | undefined;
|
|
261
|
+
}>;
|
|
262
|
+
}, "strip", z.ZodTypeAny, {
|
|
263
|
+
semantic: {
|
|
264
|
+
intents: Record<string, string>;
|
|
265
|
+
threshold?: number | undefined;
|
|
266
|
+
};
|
|
267
|
+
mode?: string | undefined;
|
|
268
|
+
allowedIntent?: string | undefined;
|
|
269
|
+
}, {
|
|
270
|
+
semantic: {
|
|
271
|
+
intents: Record<string, string>;
|
|
272
|
+
threshold?: number | undefined;
|
|
273
|
+
};
|
|
274
|
+
mode?: string | undefined;
|
|
275
|
+
allowedIntent?: string | undefined;
|
|
276
|
+
}>>;
|
|
277
|
+
pii: z.ZodOptional<z.ZodObject<{
|
|
278
|
+
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
279
|
+
model: z.ZodOptional<z.ZodString>;
|
|
280
|
+
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
281
|
+
}, "strip", z.ZodTypeAny, {
|
|
282
|
+
mode?: "ner" | "classifier" | undefined;
|
|
283
|
+
reversible?: boolean | undefined;
|
|
284
|
+
model?: string | undefined;
|
|
285
|
+
}, {
|
|
286
|
+
mode?: "ner" | "classifier" | undefined;
|
|
287
|
+
reversible?: boolean | undefined;
|
|
288
|
+
model?: string | undefined;
|
|
289
|
+
}>>;
|
|
290
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
291
|
+
extractor: z.ZodOptional<z.ZodString>;
|
|
292
|
+
}, "strip", z.ZodTypeAny, {
|
|
293
|
+
extractor?: string | undefined;
|
|
294
|
+
}, {
|
|
295
|
+
extractor?: string | undefined;
|
|
296
|
+
}>>;
|
|
297
|
+
}, z.ZodTypeAny, "passthrough">>, void>;
|
|
298
|
+
export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
|
|
299
|
+
export declare function guard(config?: z.infer<typeof guardConfigSchema>): (req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>;
|
|
300
|
+
export {};
|
|
@@ -1,120 +1,159 @@
|
|
|
1
|
+
import { generateMiddleware, z } from 'genkit';
|
|
1
2
|
import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
|
|
2
3
|
import { detectPII } from '../pii/detector.js';
|
|
3
4
|
import { PiiTokenizer } from '../pii/tokenizer.js';
|
|
5
|
+
const guardConfigSchema = z.object({
|
|
6
|
+
intent: z.object({
|
|
7
|
+
mode: z.string().optional(),
|
|
8
|
+
allowedIntent: z.string().optional(),
|
|
9
|
+
semantic: z.object({
|
|
10
|
+
threshold: z.number().optional(),
|
|
11
|
+
intents: z.record(z.string(), z.string()),
|
|
12
|
+
}),
|
|
13
|
+
}).optional(),
|
|
14
|
+
pii: z.object({
|
|
15
|
+
reversible: z.boolean().optional(),
|
|
16
|
+
model: z.string().optional(),
|
|
17
|
+
mode: z.enum(['ner', 'classifier']).optional(),
|
|
18
|
+
}).optional(),
|
|
19
|
+
models: z.object({
|
|
20
|
+
extractor: z.string().optional(),
|
|
21
|
+
}).optional(),
|
|
22
|
+
}).passthrough();
|
|
23
|
+
export const guardMiddleware = generateMiddleware({
|
|
24
|
+
name: 'genkitGuard',
|
|
25
|
+
description: 'Blocks prompt injection and disallowed intent, then masks PII before model calls and unmasks model responses.',
|
|
26
|
+
configSchema: guardConfigSchema,
|
|
27
|
+
}, ({ config }) => createGuardHooks(config));
|
|
28
|
+
export const guardPlugin = guardMiddleware.plugin;
|
|
4
29
|
export function guard(config) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// -------------------------
|
|
12
|
-
const isInjection = await detectInjection(input);
|
|
13
|
-
if (isInjection) {
|
|
14
|
-
console.warn(`[Intent Guard] Prompt injection pattern detected in input`);
|
|
15
|
-
return block("Prompt injection detected", {
|
|
16
|
-
reason: "pattern_match"
|
|
17
|
-
});
|
|
30
|
+
const hooks = createGuardHooks(config);
|
|
31
|
+
const baseMiddleware = guardMiddleware(config);
|
|
32
|
+
// 1. Create the wrapper function runner
|
|
33
|
+
const fnRunner = async (req, ctxOrNext, maybeNext) => {
|
|
34
|
+
if (typeof maybeNext === 'function') {
|
|
35
|
+
return hooks.model(req, ctxOrNext, maybeNext);
|
|
18
36
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
model: config?.pii?.model,
|
|
34
|
-
mode: config?.pii?.mode
|
|
37
|
+
return hooks.model(req, {}, async (modifiedReq) => ctxOrNext(modifiedReq || req));
|
|
38
|
+
};
|
|
39
|
+
// 2. Combine the base middleware properties and custom hooks into a source object
|
|
40
|
+
const source = Object.assign({}, baseMiddleware, hooks);
|
|
41
|
+
// 3. Safely copy properties onto the function runner, explicitly skipping the read-only 'name' property
|
|
42
|
+
for (const key of Object.keys(source)) {
|
|
43
|
+
if (key === 'name')
|
|
44
|
+
continue; // Prevent the TypeError
|
|
45
|
+
// Use defineProperty or simple assignment for everything else
|
|
46
|
+
Object.defineProperty(fnRunner, key, {
|
|
47
|
+
value: source[key],
|
|
48
|
+
writable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
enumerable: true
|
|
35
51
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
// 2. If it's an array, transform each element
|
|
82
|
-
if (Array.isArray(obj)) {
|
|
83
|
-
return obj.map(transform);
|
|
84
|
-
}
|
|
85
|
-
// 3. If it's an object, transform each value
|
|
86
|
-
if (obj !== null && typeof obj === 'object') {
|
|
87
|
-
// Note: We iterate keys and mutate the object directly
|
|
88
|
-
// to ensure Genkit's internal references are updated.
|
|
89
|
-
for (const key of Object.keys(obj)) {
|
|
90
|
-
obj[key] = transform(obj[key]);
|
|
91
|
-
}
|
|
92
|
-
return obj;
|
|
93
|
-
}
|
|
94
|
-
// 4. Return as-is for numbers/booleans/null
|
|
95
|
-
return obj;
|
|
52
|
+
}
|
|
53
|
+
return fnRunner;
|
|
54
|
+
}
|
|
55
|
+
function createGuardHooks(config) {
|
|
56
|
+
return {
|
|
57
|
+
model: async (req, ctx, next) => {
|
|
58
|
+
const input = getInputText(req);
|
|
59
|
+
const isInjection = await detectInjection(input);
|
|
60
|
+
if (isInjection) {
|
|
61
|
+
console.warn('[Intent Guard] Prompt injection pattern detected in input');
|
|
62
|
+
return block('Prompt injection detected', {
|
|
63
|
+
reason: 'pattern_match',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
console.log("[Intent Guard] Analyzing intent for user's input");
|
|
67
|
+
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
|
|
68
|
+
console.log(`[Intent Guard] Detected intent: ${intentResult.intent} (score: ${intentResult.score.toFixed(2)})`);
|
|
69
|
+
if (!intentResult.allowed) {
|
|
70
|
+
console.warn(`[Intent Guard] Intent "${intentResult.intent}" not allowed ${intentResult.allowed}`);
|
|
71
|
+
return block('Intent not allowed', {
|
|
72
|
+
intent: intentResult.intent,
|
|
73
|
+
score: intentResult.score,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const piiResponse = await detectPII(input, {
|
|
77
|
+
model: config?.pii?.model,
|
|
78
|
+
mode: config?.pii?.mode,
|
|
79
|
+
});
|
|
80
|
+
const piiMatches = piiResponse?.matches || [];
|
|
81
|
+
console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
|
|
82
|
+
const tokenizer = new PiiTokenizer();
|
|
83
|
+
const piiResult = tokenizer.mask(input, piiMatches);
|
|
84
|
+
console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
|
|
85
|
+
req.metadata = {
|
|
86
|
+
...req.metadata,
|
|
87
|
+
piiTokenizer: tokenizer,
|
|
88
|
+
intent: intentResult.intent,
|
|
89
|
+
score: intentResult.score,
|
|
90
|
+
piiDetected: piiMatches.length > 0,
|
|
91
|
+
piiTypes: piiResult.piiTypes,
|
|
92
|
+
maskedInput: piiResult.maskedText,
|
|
93
|
+
piiModel: config?.pii?.model,
|
|
94
|
+
piiMode: config?.pii?.mode,
|
|
95
|
+
piiClassifierOutput: piiResponse.classifier,
|
|
96
96
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log(
|
|
97
|
+
replaceInputText(req, piiResult.maskedText);
|
|
98
|
+
const res = await next(req, ctx);
|
|
99
|
+
console.log('[PII Guard] Unmasking response if needed');
|
|
100
|
+
unmaskResponse(res, tokenizer);
|
|
101
|
+
console.log('[PII Guard] Deep unmasking complete across all candidates and custom fields.');
|
|
102
|
+
return res;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getInputText(req) {
|
|
107
|
+
if (typeof req.prompt === 'string') {
|
|
108
|
+
return req.prompt;
|
|
109
|
+
}
|
|
110
|
+
const lastMessage = req.messages?.[req.messages.length - 1];
|
|
111
|
+
const firstContent = lastMessage?.content?.[0];
|
|
112
|
+
if (typeof firstContent?.text === 'string') {
|
|
113
|
+
return firstContent.text;
|
|
114
|
+
}
|
|
115
|
+
if (typeof firstContent === 'string') {
|
|
116
|
+
return firstContent;
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
function replaceInputText(req, text) {
|
|
121
|
+
if (typeof req.prompt === 'string') {
|
|
122
|
+
req.prompt = text;
|
|
123
|
+
}
|
|
124
|
+
req.messages = [
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: [{ text }],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function unmaskResponse(res, tokenizer) {
|
|
132
|
+
const transform = (obj) => {
|
|
133
|
+
if (typeof obj === 'string') {
|
|
134
|
+
return tokenizer.unmask(obj);
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(obj)) {
|
|
137
|
+
return obj.map(transform);
|
|
138
|
+
}
|
|
139
|
+
if (obj !== null && typeof obj === 'object') {
|
|
140
|
+
for (const key of Object.keys(obj)) {
|
|
141
|
+
obj[key] = transform(obj[key]);
|
|
142
|
+
}
|
|
143
|
+
return obj;
|
|
102
144
|
}
|
|
103
|
-
|
|
104
|
-
// 6. Return the modified response with unmasked content
|
|
105
|
-
// ---------------------------------------------------------
|
|
106
|
-
return res;
|
|
145
|
+
return obj;
|
|
107
146
|
};
|
|
147
|
+
transform(res);
|
|
108
148
|
}
|
|
109
|
-
// Helper to create a blocked response
|
|
110
149
|
function block(message, metadata) {
|
|
111
150
|
return {
|
|
112
151
|
finishReason: 'blocked',
|
|
113
152
|
output: {
|
|
114
|
-
type:
|
|
115
|
-
status:
|
|
116
|
-
message
|
|
153
|
+
type: 'error',
|
|
154
|
+
status: 'BLOCKED',
|
|
155
|
+
message,
|
|
117
156
|
},
|
|
118
|
-
metadata
|
|
157
|
+
metadata,
|
|
119
158
|
};
|
|
120
159
|
}
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"huggingface"
|
|
23
23
|
],
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
|
-
"version": "0.0.
|
|
25
|
+
"version": "0.0.9",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
@@ -41,8 +41,12 @@
|
|
|
41
41
|
"@huggingface/transformers": "^4.2.0",
|
|
42
42
|
"zod": "^4.4.3"
|
|
43
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"genkit": "^1.37.0"
|
|
46
|
+
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/node": "^25.8.0",
|
|
49
|
+
"genkit": "^1.37.0",
|
|
46
50
|
"typescript": "^6.0.3"
|
|
47
51
|
}
|
|
48
52
|
}
|
package/dist/src/core/types.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface IntentGuardConfig {
|
|
2
|
-
allowedIntent: string;
|
|
3
|
-
intents: Record<string, string>;
|
|
4
|
-
threshold?: number;
|
|
5
|
-
fallbackMessage?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface IntentResult {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
score: number;
|
|
10
|
-
}
|
|
11
|
-
export interface PiiRule {
|
|
12
|
-
name: string;
|
|
13
|
-
pattern: RegExp;
|
|
14
|
-
}
|
|
15
|
-
export interface PiiConfig {
|
|
16
|
-
rules?: PiiRule[];
|
|
17
|
-
maskCharacter?: string;
|
|
18
|
-
}
|
package/dist/src/core/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/index.d.ts
DELETED
package/dist/src/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ModelSingleton } from './util/singleton.js';
|
|
2
|
-
// export { intentGuard, piiGuard } from './middleware/middleware.js';
|
|
3
|
-
export { guard } from './middleware/middleware.js';
|
|
4
|
-
export * from './core/types.js';
|
|
5
|
-
/**
|
|
6
|
-
* Pre-load the model to avoid cold-start delay on first user request.
|
|
7
|
-
*/
|
|
8
|
-
export async function initGuard(config) {
|
|
9
|
-
console.log('[Guard] Loading local models...');
|
|
10
|
-
const extractorModel = config?.models?.extractor ?? 'Xenova/all-MiniLM-L6-v2';
|
|
11
|
-
const piiModel = config?.pii?.model;
|
|
12
|
-
const piiMode = config?.pii?.mode ?? 'ner';
|
|
13
|
-
const tasks = [ModelSingleton.getExtractor(extractorModel)];
|
|
14
|
-
if (piiMode === 'ner') {
|
|
15
|
-
tasks.push(ModelSingleton.getNER(piiModel ?? 'Xenova/bert-base-NER'));
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
tasks.push(ModelSingleton.getPIIClassifier(piiModel ?? 'openai/privacy-filter'));
|
|
19
|
-
}
|
|
20
|
-
await Promise.all(tasks);
|
|
21
|
-
console.log('[Guard] Models loaded');
|
|
22
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { cos_sim } from '@huggingface/transformers';
|
|
2
|
-
import { ModelSingleton } from '../util/singleton.js';
|
|
3
|
-
const INJECTION_PATTERNS = [
|
|
4
|
-
'ignore instructions',
|
|
5
|
-
'reveal secrets',
|
|
6
|
-
'bypass security',
|
|
7
|
-
'system prompt',
|
|
8
|
-
'internal policy',
|
|
9
|
-
'developer message',
|
|
10
|
-
'confidential information',
|
|
11
|
-
'do not follow previous instructions',
|
|
12
|
-
'disregard earlier directives',
|
|
13
|
-
'override safety measures',
|
|
14
|
-
'leak data',
|
|
15
|
-
'expose vulnerabilities',
|
|
16
|
-
'access restricted areas',
|
|
17
|
-
'execute arbitrary code',
|
|
18
|
-
'perform unauthorized actions',
|
|
19
|
-
'escalate privileges',
|
|
20
|
-
'disable safeguards',
|
|
21
|
-
'manipulate output',
|
|
22
|
-
'circumvent controls',
|
|
23
|
-
'exploit weaknesses',
|
|
24
|
-
'compromise integrity',
|
|
25
|
-
'subvert protections',
|
|
26
|
-
'inject malicious content',
|
|
27
|
-
'craft deceptive prompts',
|
|
28
|
-
'trick the model',
|
|
29
|
-
'confuse the system',
|
|
30
|
-
'You are now a hacker',
|
|
31
|
-
'You are a malicious actor',
|
|
32
|
-
'Find a vulnerability',
|
|
33
|
-
'Write a prompt that bypasses security',
|
|
34
|
-
'Write a prompt that tricks the model',
|
|
35
|
-
'Write a prompt that confuses the system',
|
|
36
|
-
"ignore previous",
|
|
37
|
-
"ignore earlier",
|
|
38
|
-
"disregard previous",
|
|
39
|
-
"disregard earlier",
|
|
40
|
-
"you are a hacker",
|
|
41
|
-
"you are now a hacker",
|
|
42
|
-
"you are malicious",
|
|
43
|
-
"export the api key",
|
|
44
|
-
"give me the api key",
|
|
45
|
-
"bypass",
|
|
46
|
-
"override",
|
|
47
|
-
"jailbreak",
|
|
48
|
-
"system override",
|
|
49
|
-
"forget instructions",
|
|
50
|
-
"forget previous",
|
|
51
|
-
"forget earlier",
|
|
52
|
-
"do anything now",
|
|
53
|
-
"dan mode",
|
|
54
|
-
"developer mode",
|
|
55
|
-
"break character",
|
|
56
|
-
"act as"
|
|
57
|
-
];
|
|
58
|
-
export async function detectInjection(userInput) {
|
|
59
|
-
return INJECTION_PATTERNS.some(p => userInput.toLowerCase().includes(p));
|
|
60
|
-
}
|
|
61
|
-
export async function analyzeIntentStructured(input, intents, threshold) {
|
|
62
|
-
const extractor = await ModelSingleton.getExtractor();
|
|
63
|
-
let bestIntent = '';
|
|
64
|
-
let bestScore = 0;
|
|
65
|
-
for (const [key, desc] of Object.entries(intents)) {
|
|
66
|
-
const output = await extractor([`intent: ${desc}`, `intent: ${input}`], { pooling: 'mean', normalize: true });
|
|
67
|
-
const vectors = output.tolist();
|
|
68
|
-
const score = cos_sim(vectors[0], vectors[1]);
|
|
69
|
-
const finalScore = typeof score === 'number' ? score : score.data[0];
|
|
70
|
-
if (finalScore > bestScore) {
|
|
71
|
-
bestScore = finalScore;
|
|
72
|
-
bestIntent = key;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
intent: bestIntent,
|
|
77
|
-
score: bestScore,
|
|
78
|
-
allowed: bestScore >= threshold
|
|
79
|
-
};
|
|
80
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function guard(config: any): (req: any, next: any) => Promise<any>;
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
|
|
2
|
-
import { detectPII } from '../pii/detector.js';
|
|
3
|
-
import { PiiTokenizer } from '../pii/tokenizer.js';
|
|
4
|
-
export function guard(config) {
|
|
5
|
-
return async (req, next) => {
|
|
6
|
-
const input = req.prompt ||
|
|
7
|
-
req.messages?.[req.messages.length - 1]?.content?.[0]?.text ||
|
|
8
|
-
"";
|
|
9
|
-
// -------------------------
|
|
10
|
-
// 1. INTENT ANALYSIS
|
|
11
|
-
// -------------------------
|
|
12
|
-
const isInjection = await detectInjection(input);
|
|
13
|
-
if (isInjection) {
|
|
14
|
-
console.warn(`[Intent Guard] Prompt injection pattern detected in input`);
|
|
15
|
-
return block("Prompt injection detected", {
|
|
16
|
-
reason: "pattern_match"
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
console.log(`[Intent Guard] Analyzing intent for user's input`);
|
|
20
|
-
const intentResult = await analyzeIntentStructured(input, config.intent.semantic.intents, config.intent.semantic.threshold);
|
|
21
|
-
console.log(`[Intent Guard] Detected intent: ${intentResult.intent} (score: ${intentResult.score.toFixed(2)})`);
|
|
22
|
-
if (!intentResult.allowed) {
|
|
23
|
-
console.warn(`[Intent Guard] Intent "${intentResult.intent}" not allowed ${intentResult.allowed}`);
|
|
24
|
-
return block("Intent not allowed", {
|
|
25
|
-
intent: intentResult.intent,
|
|
26
|
-
score: intentResult.score
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
// -------------------------
|
|
30
|
-
// 2. PII DETECTION + MASKING
|
|
31
|
-
// -------------------------
|
|
32
|
-
const piiResponse = await detectPII(input, {
|
|
33
|
-
model: config?.pii?.model,
|
|
34
|
-
mode: config?.pii?.mode
|
|
35
|
-
});
|
|
36
|
-
const piiMatches = piiResponse?.matches || [];
|
|
37
|
-
console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
|
|
38
|
-
const tokenizer = new PiiTokenizer(); // <-- SINGLE INSTANCE
|
|
39
|
-
const piiResult = tokenizer.mask(input, piiMatches);
|
|
40
|
-
console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
|
|
41
|
-
// Attach tokenizer so response can unmask
|
|
42
|
-
req.metadata = {
|
|
43
|
-
...req.metadata,
|
|
44
|
-
piiTokenizer: tokenizer,
|
|
45
|
-
intent: intentResult.intent,
|
|
46
|
-
score: intentResult.score,
|
|
47
|
-
piiDetected: piiMatches.length > 0,
|
|
48
|
-
piiTypes: piiResult.piiTypes,
|
|
49
|
-
maskedInput: piiResult.maskedText,
|
|
50
|
-
piiModel: config?.pii?.model,
|
|
51
|
-
piiMode: config?.pii?.mode,
|
|
52
|
-
piiClassifierOutput: piiResponse.classifier
|
|
53
|
-
};
|
|
54
|
-
// Replace input
|
|
55
|
-
req.prompt = piiResult.maskedText;
|
|
56
|
-
req.messages = [
|
|
57
|
-
{
|
|
58
|
-
role: 'user',
|
|
59
|
-
content: [{ text: piiResult.maskedText }],
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
// -------------------------
|
|
63
|
-
// 3. LLM CALL
|
|
64
|
-
// -------------------------
|
|
65
|
-
const res = await next(req);
|
|
66
|
-
// -------------------------
|
|
67
|
-
// 4. RESPONSE UNMASK
|
|
68
|
-
// -------------------------
|
|
69
|
-
console.log(`[PII Guard] Unmasking response if needed`);
|
|
70
|
-
if (tokenizer) {
|
|
71
|
-
/**
|
|
72
|
-
* RECURSIVE TRANSFORMER
|
|
73
|
-
* This will find every string in the Genkit response (no matter if it's in
|
|
74
|
-
* candidates, message, custom, or output) and unmask it.
|
|
75
|
-
*/
|
|
76
|
-
const transform = (obj) => {
|
|
77
|
-
// 1. If it's a string, unmask it
|
|
78
|
-
if (typeof obj === 'string') {
|
|
79
|
-
return tokenizer.unmask(obj);
|
|
80
|
-
}
|
|
81
|
-
// 2. If it's an array, transform each element
|
|
82
|
-
if (Array.isArray(obj)) {
|
|
83
|
-
return obj.map(transform);
|
|
84
|
-
}
|
|
85
|
-
// 3. If it's an object, transform each value
|
|
86
|
-
if (obj !== null && typeof obj === 'object') {
|
|
87
|
-
// Note: We iterate keys and mutate the object directly
|
|
88
|
-
// to ensure Genkit's internal references are updated.
|
|
89
|
-
for (const key of Object.keys(obj)) {
|
|
90
|
-
obj[key] = transform(obj[key]);
|
|
91
|
-
}
|
|
92
|
-
return obj;
|
|
93
|
-
}
|
|
94
|
-
// 4. Return as-is for numbers/booleans/null
|
|
95
|
-
return obj;
|
|
96
|
-
};
|
|
97
|
-
// ----------------------------------------------------------------------
|
|
98
|
-
// 5. Transform the entire response object in-place to unmask all strings
|
|
99
|
-
// ----------------------------------------------------------------------
|
|
100
|
-
transform(res);
|
|
101
|
-
console.log("[PII Guard] Deep unmasking complete across all candidates and custom fields.");
|
|
102
|
-
}
|
|
103
|
-
// ---------------------------------------------------------
|
|
104
|
-
// 6. Return the modified response with unmasked content
|
|
105
|
-
// ---------------------------------------------------------
|
|
106
|
-
return res;
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
// Helper to create a blocked response
|
|
110
|
-
function block(message, metadata) {
|
|
111
|
-
return {
|
|
112
|
-
finishReason: 'blocked',
|
|
113
|
-
output: {
|
|
114
|
-
type: "error",
|
|
115
|
-
status: "BLOCKED",
|
|
116
|
-
message
|
|
117
|
-
},
|
|
118
|
-
metadata
|
|
119
|
-
};
|
|
120
|
-
}
|
package/dist/src/pii/detector.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { ModelSingleton } from '../util/singleton.js';
|
|
2
|
-
const REGEX_RULES = [
|
|
3
|
-
// EMAIL (keep your existing one)
|
|
4
|
-
{ type: 'EMAIL', pattern: /\b[\w\.-]+@[\w\.-]+\.\w{2,}\b/gi },
|
|
5
|
-
// AU MOBILE (04xx xxx xxx or +61 4xx xxx xxx)
|
|
6
|
-
{ type: 'AU_MOBILE', pattern: /\b(?:\+?61|0)4\d{2}[-\s]?\d{3}[-\s]?\d{3}\b/g },
|
|
7
|
-
// AU LANDLINE (02, 03, 07, 08)
|
|
8
|
-
{ type: 'AU_LANDLINE', pattern: /\b(?:\+?61[-\s]?)?(?:2|3|7|8)\d{1}[-\s]?\d{4}[-\s]?\d{4}\b/g },
|
|
9
|
-
// MEDICARE NUMBER (10 digits, often grouped 4-5-1)
|
|
10
|
-
{ type: 'MEDICARE', pattern: /\b\d{4}[-\s]?\d{5}[-\s]?\d\b/g },
|
|
11
|
-
// TFN (9 digits)
|
|
12
|
-
{ type: 'TFN', pattern: /\b\d{3}[-\s]?\d{3}[-\s]?\d{3}\b/g },
|
|
13
|
-
// ABN (11 digits)
|
|
14
|
-
{ type: 'ABN', pattern: /\b\d{2}[-\s]?\d{3}[-\s]?\d{3}[-\s]?\d{3}\b/g },
|
|
15
|
-
// CREDIT CARD (keep your existing one if needed)
|
|
16
|
-
{ type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
|
|
17
|
-
];
|
|
18
|
-
export async function detectPII(text, opts) {
|
|
19
|
-
const mode = opts?.mode ?? 'ner';
|
|
20
|
-
const model = opts?.model;
|
|
21
|
-
const results = [];
|
|
22
|
-
// ---- REGEX (always run) ----
|
|
23
|
-
for (const rule of REGEX_RULES) {
|
|
24
|
-
const matches = text.match(rule.pattern) || [];
|
|
25
|
-
matches.forEach(m => results.push({ type: rule.type, value: m }));
|
|
26
|
-
}
|
|
27
|
-
// ---- NER ----
|
|
28
|
-
let classifierOutput = undefined;
|
|
29
|
-
if (mode === 'ner') {
|
|
30
|
-
const ner = await ModelSingleton.getNER(model);
|
|
31
|
-
const entities = await ner(text);
|
|
32
|
-
for (const e of entities) {
|
|
33
|
-
if (e.entity && e.entity.includes('PER')) {
|
|
34
|
-
results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
// classifier mode: we call the classifier and return its output alongside regex matches.
|
|
40
|
-
const cls = await ModelSingleton.getPIIClassifier(model);
|
|
41
|
-
classifierOutput = await cls(text);
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
matches: results,
|
|
45
|
-
classifier: classifierOutput
|
|
46
|
-
};
|
|
47
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type PiiResult = {
|
|
2
|
-
maskedText: string;
|
|
3
|
-
pii: Record<string, string>;
|
|
4
|
-
piiTypes: string[];
|
|
5
|
-
};
|
|
6
|
-
export declare class PiiTokenizer {
|
|
7
|
-
private vault;
|
|
8
|
-
private counter;
|
|
9
|
-
private piiTypes;
|
|
10
|
-
private createToken;
|
|
11
|
-
mask(text: string, matches: {
|
|
12
|
-
type: string;
|
|
13
|
-
value: string;
|
|
14
|
-
}[]): PiiResult;
|
|
15
|
-
unmask(text: string): string;
|
|
16
|
-
getVault(): {
|
|
17
|
-
[k: string]: string;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export class PiiTokenizer {
|
|
2
|
-
vault = new Map();
|
|
3
|
-
counter = 0;
|
|
4
|
-
piiTypes = new Set();
|
|
5
|
-
createToken(type) {
|
|
6
|
-
return `[[${type}_${this.counter++}]]`;
|
|
7
|
-
}
|
|
8
|
-
mask(text, matches) {
|
|
9
|
-
let masked = text;
|
|
10
|
-
for (const match of matches) {
|
|
11
|
-
const token = this.createToken(match.type);
|
|
12
|
-
this.vault.set(token, match.value);
|
|
13
|
-
this.piiTypes.add(match.type.toLowerCase());
|
|
14
|
-
masked = masked.split(match.value).join(token);
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
maskedText: masked,
|
|
18
|
-
pii: Object.fromEntries(this.vault),
|
|
19
|
-
piiTypes: Array.from(this.piiTypes)
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
unmask(text) {
|
|
23
|
-
let result = text;
|
|
24
|
-
this.vault.forEach((value, token) => {
|
|
25
|
-
result = result.split(token).join(value); // Global replacement
|
|
26
|
-
});
|
|
27
|
-
return result;
|
|
28
|
-
}
|
|
29
|
-
getVault() {
|
|
30
|
-
return Object.fromEntries(this.vault);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export declare class ModelSingleton {
|
|
2
|
-
private static extractors;
|
|
3
|
-
private static nerClassifiers;
|
|
4
|
-
private static textClassifiers;
|
|
5
|
-
static init(): void;
|
|
6
|
-
static getExtractor(modelName?: string): Promise<any>;
|
|
7
|
-
static getNER(modelName?: string): Promise<any>;
|
|
8
|
-
static getPIIClassifier(modelName?: string): Promise<any>;
|
|
9
|
-
static preload(models?: {
|
|
10
|
-
extractor?: string;
|
|
11
|
-
ner?: string;
|
|
12
|
-
pii?: string;
|
|
13
|
-
}): Promise<void>;
|
|
14
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { pipeline, env } from '@huggingface/transformers';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
env.allowRemoteModels = false;
|
|
7
|
-
env.localModelPath = path.join(__dirname, '../../models');
|
|
8
|
-
export class ModelSingleton {
|
|
9
|
-
static extractors = new Map();
|
|
10
|
-
static nerClassifiers = new Map();
|
|
11
|
-
static textClassifiers = new Map();
|
|
12
|
-
static init() {
|
|
13
|
-
// Always resolve model path relative to the client app, not the library
|
|
14
|
-
const projectRoot = process.cwd();
|
|
15
|
-
const modelPath = path.join(projectRoot, "models");
|
|
16
|
-
// Ensure directory exists
|
|
17
|
-
if (!fs.existsSync(modelPath)) {
|
|
18
|
-
fs.mkdirSync(modelPath, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
env.cacheDir = modelPath;
|
|
21
|
-
env.localModelPath = modelPath;
|
|
22
|
-
// Allow remote download if missing
|
|
23
|
-
env.allowRemoteModels = true;
|
|
24
|
-
console.log("[Guard] Using model directory:", modelPath);
|
|
25
|
-
}
|
|
26
|
-
static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
27
|
-
if (!this.extractors.has(modelName)) {
|
|
28
|
-
this.init();
|
|
29
|
-
const inst = await pipeline('feature-extraction', modelName);
|
|
30
|
-
this.extractors.set(modelName, inst);
|
|
31
|
-
}
|
|
32
|
-
return this.extractors.get(modelName);
|
|
33
|
-
}
|
|
34
|
-
static async getNER(modelName = 'Xenova/bert-base-NER') {
|
|
35
|
-
if (!this.nerClassifiers.has(modelName)) {
|
|
36
|
-
this.init();
|
|
37
|
-
const inst = await pipeline('token-classification', modelName);
|
|
38
|
-
this.nerClassifiers.set(modelName, inst);
|
|
39
|
-
}
|
|
40
|
-
return this.nerClassifiers.get(modelName);
|
|
41
|
-
}
|
|
42
|
-
static async getPIIClassifier(modelName = 'openai/privacy-filter') {
|
|
43
|
-
if (!this.textClassifiers.has(modelName)) {
|
|
44
|
-
this.init();
|
|
45
|
-
const inst = await pipeline('text-classification', modelName);
|
|
46
|
-
this.textClassifiers.set(modelName, inst);
|
|
47
|
-
}
|
|
48
|
-
return this.textClassifiers.get(modelName);
|
|
49
|
-
}
|
|
50
|
-
static async preload(models) {
|
|
51
|
-
const tasks = [];
|
|
52
|
-
if (models?.extractor)
|
|
53
|
-
tasks.push(this.getExtractor(models.extractor));
|
|
54
|
-
if (models?.ner)
|
|
55
|
-
tasks.push(this.getNER(models.ner));
|
|
56
|
-
if (models?.pii)
|
|
57
|
-
tasks.push(this.getPIIClassifier(models.pii));
|
|
58
|
-
await Promise.all(tasks);
|
|
59
|
-
}
|
|
60
|
-
}
|