@intflows/genkit-guard 0.0.9 → 0.0.11
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 +63 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +24 -3
- package/dist/middleware/middleware.d.ts +118 -142
- package/dist/middleware/middleware.js +220 -47
- package/dist/pii/storage.d.ts +42 -0
- package/dist/pii/storage.js +64 -0
- package/dist/pii/tokenizer.d.ts +15 -5
- package/dist/pii/tokenizer.js +49 -12
- package/dist/util/singleton.js +16 -1
- package/package.json +8 -3
- package/scripts/test-concurrent-vault.js +96 -0
- package/scripts/test-redis-vault.js +153 -0
- package/scripts/test-storage.js +78 -0
- package/scripts/test-types.ts +21 -0
|
@@ -2,6 +2,8 @@ import { generateMiddleware, z } from 'genkit';
|
|
|
2
2
|
import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
|
|
3
3
|
import { detectPII } from '../pii/detector.js';
|
|
4
4
|
import { PiiTokenizer } from '../pii/tokenizer.js';
|
|
5
|
+
import { defaultPiiVaultStorage } from '../pii/storage.js';
|
|
6
|
+
const GUARD_CONTEXT_KEY = '__genkitGuard';
|
|
5
7
|
const guardConfigSchema = z.object({
|
|
6
8
|
intent: z.object({
|
|
7
9
|
mode: z.string().optional(),
|
|
@@ -15,6 +17,15 @@ const guardConfigSchema = z.object({
|
|
|
15
17
|
reversible: z.boolean().optional(),
|
|
16
18
|
model: z.string().optional(),
|
|
17
19
|
mode: z.enum(['ner', 'classifier']).optional(),
|
|
20
|
+
vault: z.object({
|
|
21
|
+
storage: z.any().optional(),
|
|
22
|
+
scopeId: z.any().optional(),
|
|
23
|
+
}).optional(),
|
|
24
|
+
}).optional(),
|
|
25
|
+
logging: z.object({
|
|
26
|
+
enabled: z.boolean().optional(),
|
|
27
|
+
level: z.enum(['debug', 'info', 'warn', 'error']).optional(),
|
|
28
|
+
serviceName: z.string().optional(),
|
|
18
29
|
}).optional(),
|
|
19
30
|
models: z.object({
|
|
20
31
|
extractor: z.string().optional(),
|
|
@@ -22,83 +33,132 @@ const guardConfigSchema = z.object({
|
|
|
22
33
|
}).passthrough();
|
|
23
34
|
export const guardMiddleware = generateMiddleware({
|
|
24
35
|
name: 'genkitGuard',
|
|
25
|
-
description: 'Blocks prompt injection and disallowed intent,
|
|
36
|
+
description: 'Blocks prompt injection and disallowed intent, masks PII before model calls, restores PII for tool calls, and audits tool PII access.',
|
|
26
37
|
configSchema: guardConfigSchema,
|
|
27
38
|
}, ({ config }) => createGuardHooks(config));
|
|
28
39
|
export const guardPlugin = guardMiddleware.plugin;
|
|
29
40
|
export function guard(config) {
|
|
30
41
|
const hooks = createGuardHooks(config);
|
|
31
42
|
const baseMiddleware = guardMiddleware(config);
|
|
32
|
-
// 1. Create the wrapper function runner
|
|
33
43
|
const fnRunner = async (req, ctxOrNext, maybeNext) => {
|
|
34
44
|
if (typeof maybeNext === 'function') {
|
|
35
45
|
return hooks.model(req, ctxOrNext, maybeNext);
|
|
36
46
|
}
|
|
37
47
|
return hooks.model(req, {}, async (modifiedReq) => ctxOrNext(modifiedReq || req));
|
|
38
48
|
};
|
|
39
|
-
// 2. Combine the base middleware properties and custom hooks into a source object
|
|
40
49
|
const source = Object.assign({}, baseMiddleware, hooks);
|
|
41
|
-
// 3. Safely copy properties onto the function runner, explicitly skipping the read-only 'name' property
|
|
42
50
|
for (const key of Object.keys(source)) {
|
|
43
51
|
if (key === 'name')
|
|
44
|
-
continue;
|
|
45
|
-
// Use defineProperty or simple assignment for everything else
|
|
52
|
+
continue;
|
|
46
53
|
Object.defineProperty(fnRunner, key, {
|
|
47
54
|
value: source[key],
|
|
48
55
|
writable: true,
|
|
49
56
|
configurable: true,
|
|
50
|
-
enumerable: true
|
|
57
|
+
enumerable: true,
|
|
51
58
|
});
|
|
52
59
|
}
|
|
53
60
|
return fnRunner;
|
|
54
61
|
}
|
|
62
|
+
export const guardAction = guard;
|
|
55
63
|
function createGuardHooks(config) {
|
|
64
|
+
const logger = createLogger(config);
|
|
56
65
|
return {
|
|
57
66
|
model: async (req, ctx, next) => {
|
|
58
67
|
const input = getInputText(req);
|
|
68
|
+
logger('info', 'guard.model.start', 'Starting guard checks for model request');
|
|
59
69
|
const isInjection = await detectInjection(input);
|
|
60
70
|
if (isInjection) {
|
|
61
|
-
|
|
71
|
+
logger('warn', 'guard.intent.blocked', 'Prompt injection pattern detected', {
|
|
72
|
+
reason: 'pattern_match',
|
|
73
|
+
});
|
|
62
74
|
return block('Prompt injection detected', {
|
|
63
75
|
reason: 'pattern_match',
|
|
64
76
|
});
|
|
65
77
|
}
|
|
66
|
-
|
|
78
|
+
logger('info', 'guard.intent.analysis.start', 'Analyzing request intent');
|
|
67
79
|
const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
|
|
68
|
-
|
|
80
|
+
logger('info', 'guard.intent.analysis.complete', 'Intent analysis completed', {
|
|
81
|
+
intent: intentResult.intent,
|
|
82
|
+
score: roundScore(intentResult.score),
|
|
83
|
+
allowed: intentResult.allowed,
|
|
84
|
+
});
|
|
69
85
|
if (!intentResult.allowed) {
|
|
70
|
-
|
|
86
|
+
logger('warn', 'guard.intent.blocked', 'Intent not allowed', {
|
|
87
|
+
intent: intentResult.intent,
|
|
88
|
+
score: roundScore(intentResult.score),
|
|
89
|
+
});
|
|
71
90
|
return block('Intent not allowed', {
|
|
72
91
|
intent: intentResult.intent,
|
|
73
92
|
score: intentResult.score,
|
|
74
93
|
});
|
|
75
94
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
mode: config?.pii?.mode,
|
|
79
|
-
});
|
|
95
|
+
const textForPii = collectModelRequestText(req);
|
|
96
|
+
const piiResponse = await scanPII(textForPii, config);
|
|
80
97
|
const piiMatches = piiResponse?.matches || [];
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
const tokenizer = createTokenizer(config, req, ctx);
|
|
99
|
+
const piiTypes = uniqueTypes(piiMatches);
|
|
100
|
+
await tokenizer.importTokens(textForPii);
|
|
101
|
+
await maskModelRequest(req, tokenizer, piiMatches);
|
|
102
|
+
pushTokenizer(ctx, tokenizer);
|
|
103
|
+
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.model.pii.masked', 'PII scan completed for model request', {
|
|
104
|
+
piiDetected: piiMatches.length > 0,
|
|
105
|
+
piiMatchCount: piiMatches.length,
|
|
106
|
+
piiTypes,
|
|
107
|
+
piiMode: config?.pii?.mode ?? 'ner',
|
|
108
|
+
classifierOutputPresent: Boolean(piiResponse.classifier),
|
|
109
|
+
});
|
|
85
110
|
req.metadata = {
|
|
86
111
|
...req.metadata,
|
|
87
|
-
piiTokenizer: tokenizer,
|
|
88
112
|
intent: intentResult.intent,
|
|
89
113
|
score: intentResult.score,
|
|
90
114
|
piiDetected: piiMatches.length > 0,
|
|
91
|
-
piiTypes
|
|
92
|
-
maskedInput:
|
|
115
|
+
piiTypes,
|
|
116
|
+
maskedInput: getInputText(req),
|
|
93
117
|
piiModel: config?.pii?.model,
|
|
94
118
|
piiMode: config?.pii?.mode,
|
|
95
119
|
piiClassifierOutput: piiResponse.classifier,
|
|
96
120
|
};
|
|
97
|
-
replaceInputText(req, piiResult.maskedText);
|
|
98
121
|
const res = await next(req, ctx);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
const unmaskedResponse = await unmaskObject(res, getGuardState(ctx).tokenizers);
|
|
123
|
+
logger('info', 'guard.model.response.unmasked', 'Model response unmasked for downstream execution', {
|
|
124
|
+
piiTypes,
|
|
125
|
+
});
|
|
126
|
+
return unmaskedResponse;
|
|
127
|
+
},
|
|
128
|
+
tool: async (req, ctx, next) => {
|
|
129
|
+
const state = getGuardState(ctx);
|
|
130
|
+
const toolName = req?.toolRequest?.name;
|
|
131
|
+
if (req?.toolRequest && 'input' in req.toolRequest) {
|
|
132
|
+
req.toolRequest.input = await unmaskObject(req.toolRequest.input, state.tokenizers);
|
|
133
|
+
}
|
|
134
|
+
const toolInputText = collectStrings(req?.toolRequest?.input).join('\n');
|
|
135
|
+
const piiResponse = await scanPII(toolInputText, config);
|
|
136
|
+
const piiMatches = piiResponse?.matches || [];
|
|
137
|
+
const piiTypes = uniqueTypes(piiMatches);
|
|
138
|
+
req.metadata = {
|
|
139
|
+
...req.metadata,
|
|
140
|
+
piiDetected: piiMatches.length > 0,
|
|
141
|
+
piiTypes,
|
|
142
|
+
piiMatchCount: piiMatches.length,
|
|
143
|
+
};
|
|
144
|
+
logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.tool.pii.checked', 'Tool request PII scan completed', {
|
|
145
|
+
toolName,
|
|
146
|
+
piiDetected: piiMatches.length > 0,
|
|
147
|
+
piiMatchCount: piiMatches.length,
|
|
148
|
+
piiTypes,
|
|
149
|
+
});
|
|
150
|
+
const res = await next(req, ctx);
|
|
151
|
+
const toolResponseText = collectStrings(res).join('\n');
|
|
152
|
+
if (toolResponseText) {
|
|
153
|
+
const responsePii = await scanPII(toolResponseText, config);
|
|
154
|
+
const responseMatches = responsePii?.matches || [];
|
|
155
|
+
logger(responseMatches.length > 0 ? 'warn' : 'info', 'guard.tool.response.pii.checked', 'Tool response PII scan completed', {
|
|
156
|
+
toolName,
|
|
157
|
+
piiDetected: responseMatches.length > 0,
|
|
158
|
+
piiMatchCount: responseMatches.length,
|
|
159
|
+
piiTypes: uniqueTypes(responseMatches),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
102
162
|
return res;
|
|
103
163
|
},
|
|
104
164
|
};
|
|
@@ -115,36 +175,149 @@ function getInputText(req) {
|
|
|
115
175
|
if (typeof firstContent === 'string') {
|
|
116
176
|
return firstContent;
|
|
117
177
|
}
|
|
118
|
-
return '';
|
|
178
|
+
return collectStrings(lastMessage).join('\n');
|
|
119
179
|
}
|
|
120
|
-
function
|
|
180
|
+
function collectModelRequestText(req) {
|
|
181
|
+
return [
|
|
182
|
+
...collectStrings(req?.prompt),
|
|
183
|
+
...collectStrings(req?.messages),
|
|
184
|
+
...collectStrings(req?.docs),
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
async function maskModelRequest(req, tokenizer, matches) {
|
|
121
188
|
if (typeof req.prompt === 'string') {
|
|
122
|
-
req.prompt =
|
|
189
|
+
req.prompt = (await tokenizer.mask(req.prompt, matches)).maskedText;
|
|
190
|
+
}
|
|
191
|
+
if (req.messages) {
|
|
192
|
+
req.messages = await transformStrings(req.messages, async (value) => (await tokenizer.mask(value, matches)).maskedText);
|
|
193
|
+
}
|
|
194
|
+
if (req.docs) {
|
|
195
|
+
req.docs = await transformStrings(req.docs, async (value) => (await tokenizer.mask(value, matches)).maskedText);
|
|
123
196
|
}
|
|
124
|
-
req.messages = [
|
|
125
|
-
{
|
|
126
|
-
role: 'user',
|
|
127
|
-
content: [{ text }],
|
|
128
|
-
},
|
|
129
|
-
];
|
|
130
197
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
198
|
+
async function unmaskObject(obj, tokenizers) {
|
|
199
|
+
return transformStrings(obj, async (value) => {
|
|
200
|
+
let result = value;
|
|
201
|
+
for (const tokenizer of tokenizers) {
|
|
202
|
+
result = await tokenizer.unmask(result);
|
|
135
203
|
}
|
|
136
|
-
|
|
137
|
-
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function transformStrings(obj, transform) {
|
|
208
|
+
if (typeof obj === 'string') {
|
|
209
|
+
return await transform(obj);
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(obj)) {
|
|
212
|
+
for (let i = 0; i < obj.length; i++) {
|
|
213
|
+
obj[i] = await transformStrings(obj[i], transform);
|
|
138
214
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
return obj;
|
|
216
|
+
}
|
|
217
|
+
if (obj !== null && typeof obj === 'object') {
|
|
218
|
+
for (const key of Object.keys(obj)) {
|
|
219
|
+
obj[key] = await transformStrings(obj[key], transform);
|
|
144
220
|
}
|
|
145
221
|
return obj;
|
|
222
|
+
}
|
|
223
|
+
return obj;
|
|
224
|
+
}
|
|
225
|
+
function collectStrings(obj) {
|
|
226
|
+
if (typeof obj === 'string') {
|
|
227
|
+
return [obj];
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(obj)) {
|
|
230
|
+
return obj.flatMap(collectStrings);
|
|
231
|
+
}
|
|
232
|
+
if (obj !== null && typeof obj === 'object') {
|
|
233
|
+
return Object.values(obj).flatMap(collectStrings);
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
async function scanPII(text, config) {
|
|
238
|
+
if (!text.trim()) {
|
|
239
|
+
return {
|
|
240
|
+
matches: [],
|
|
241
|
+
classifier: undefined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return detectPII(text, {
|
|
245
|
+
model: config?.pii?.model,
|
|
246
|
+
mode: config?.pii?.mode,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function getGuardState(ctx = {}) {
|
|
250
|
+
ctx.context = ctx.context || {};
|
|
251
|
+
ctx.context[GUARD_CONTEXT_KEY] = ctx.context[GUARD_CONTEXT_KEY] || { tokenizers: [] };
|
|
252
|
+
return ctx.context[GUARD_CONTEXT_KEY];
|
|
253
|
+
}
|
|
254
|
+
function pushTokenizer(ctx, tokenizer) {
|
|
255
|
+
const state = getGuardState(ctx);
|
|
256
|
+
state.tokenizers.push(tokenizer);
|
|
257
|
+
}
|
|
258
|
+
function createTokenizer(config, req, ctx) {
|
|
259
|
+
const configuredScope = config?.pii?.vault?.scopeId;
|
|
260
|
+
const scopeId = typeof configuredScope === 'function'
|
|
261
|
+
? configuredScope(req, ctx)
|
|
262
|
+
: configuredScope;
|
|
263
|
+
return new PiiTokenizer({
|
|
264
|
+
scopeId: typeof scopeId === 'string' ? scopeId : undefined,
|
|
265
|
+
storage: config?.pii?.vault?.storage ?? defaultPiiVaultStorage,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function uniqueTypes(matches) {
|
|
269
|
+
return Array.from(new Set(matches.map((match) => match.type.toLowerCase())));
|
|
270
|
+
}
|
|
271
|
+
function roundScore(score) {
|
|
272
|
+
return Math.round(score * 10000) / 10000;
|
|
273
|
+
}
|
|
274
|
+
function createLogger(config) {
|
|
275
|
+
const enabled = config?.logging?.enabled ?? true;
|
|
276
|
+
const minimumLevel = config?.logging?.level ?? 'info';
|
|
277
|
+
const serviceName = config?.logging?.serviceName ?? '@intflows/genkit-guard';
|
|
278
|
+
const levelRank = {
|
|
279
|
+
debug: 10,
|
|
280
|
+
info: 20,
|
|
281
|
+
warn: 30,
|
|
282
|
+
error: 40,
|
|
283
|
+
};
|
|
284
|
+
const severityNumber = {
|
|
285
|
+
debug: 5,
|
|
286
|
+
info: 9,
|
|
287
|
+
warn: 13,
|
|
288
|
+
error: 17,
|
|
289
|
+
};
|
|
290
|
+
return (severity, eventName, body, attributes = {}) => {
|
|
291
|
+
if (!enabled || levelRank[severity] < levelRank[minimumLevel]) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const record = {
|
|
295
|
+
timestamp: new Date().toISOString(),
|
|
296
|
+
severityText: severity.toUpperCase(),
|
|
297
|
+
severityNumber: severityNumber[severity],
|
|
298
|
+
body,
|
|
299
|
+
resource: {
|
|
300
|
+
attributes: {
|
|
301
|
+
'service.name': serviceName,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
attributes: {
|
|
305
|
+
'event.name': eventName,
|
|
306
|
+
'code.namespace': 'genkit-guard',
|
|
307
|
+
...attributes,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const line = JSON.stringify(record);
|
|
311
|
+
if (severity === 'error') {
|
|
312
|
+
console.error(line);
|
|
313
|
+
}
|
|
314
|
+
else if (severity === 'warn') {
|
|
315
|
+
console.warn(line);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
console.log(line);
|
|
319
|
+
}
|
|
146
320
|
};
|
|
147
|
-
transform(res);
|
|
148
321
|
}
|
|
149
322
|
function block(message, metadata) {
|
|
150
323
|
return {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PiiVaultEntry = {
|
|
2
|
+
token: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
export interface PiiVaultStorage {
|
|
6
|
+
get(scopeId: string, token: string): string | undefined | Promise<string | undefined>;
|
|
7
|
+
getByToken?(token: string): string | undefined | Promise<string | undefined>;
|
|
8
|
+
set(scopeId: string, token: string, value: string): void | Promise<void>;
|
|
9
|
+
entries(scopeId: string): PiiVaultEntry[] | Promise<PiiVaultEntry[]>;
|
|
10
|
+
}
|
|
11
|
+
export type PiiVaultStorageAdapter = {
|
|
12
|
+
get: PiiVaultStorage['get'];
|
|
13
|
+
getByToken?: PiiVaultStorage['getByToken'];
|
|
14
|
+
set: PiiVaultStorage['set'];
|
|
15
|
+
entries: PiiVaultStorage['entries'];
|
|
16
|
+
};
|
|
17
|
+
export declare function createPiiVaultStorage(adapter: PiiVaultStorageAdapter): PiiVaultStorage;
|
|
18
|
+
export type RedisPiiVaultClient = {
|
|
19
|
+
hGet?: (key: string, field: string) => Promise<string | null | undefined>;
|
|
20
|
+
hSet?: (key: string, field: string, value: string) => Promise<unknown>;
|
|
21
|
+
hGetAll?: (key: string) => Promise<Record<string, string>>;
|
|
22
|
+
hget?: (key: string, field: string) => Promise<string | null | undefined>;
|
|
23
|
+
hset?: (key: string, field: string, value: string) => Promise<unknown>;
|
|
24
|
+
hgetall?: (key: string) => Promise<Record<string, string>>;
|
|
25
|
+
expire?: (key: string, seconds: number) => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
export type RedisPiiVaultStorageOptions = {
|
|
28
|
+
keyPrefix?: string;
|
|
29
|
+
tokenIndexKey?: string;
|
|
30
|
+
ttlSeconds?: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function createRedisPiiVaultStorage(redis: RedisPiiVaultClient, options?: RedisPiiVaultStorageOptions): PiiVaultStorage;
|
|
33
|
+
export declare class InMemoryPiiVaultStorage implements PiiVaultStorage {
|
|
34
|
+
private scopes;
|
|
35
|
+
private tokenIndex;
|
|
36
|
+
get(scopeId: string, token: string): string | undefined;
|
|
37
|
+
getByToken(token: string): string | undefined;
|
|
38
|
+
set(scopeId: string, token: string, value: string): void;
|
|
39
|
+
entries(scopeId: string): PiiVaultEntry[];
|
|
40
|
+
private getScope;
|
|
41
|
+
}
|
|
42
|
+
export declare const defaultPiiVaultStorage: InMemoryPiiVaultStorage;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function createPiiVaultStorage(adapter) {
|
|
2
|
+
return adapter;
|
|
3
|
+
}
|
|
4
|
+
export function createRedisPiiVaultStorage(redis, options = {}) {
|
|
5
|
+
const keyPrefix = options.keyPrefix ?? 'genkit-guard:pii';
|
|
6
|
+
const tokenIndexKey = options.tokenIndexKey ?? `${keyPrefix}:tokens`;
|
|
7
|
+
const hGet = redis.hGet?.bind(redis) ?? redis.hget?.bind(redis);
|
|
8
|
+
const hSet = redis.hSet?.bind(redis) ?? redis.hset?.bind(redis);
|
|
9
|
+
const hGetAll = redis.hGetAll?.bind(redis) ?? redis.hgetall?.bind(redis);
|
|
10
|
+
if (!hGet || !hSet || !hGetAll) {
|
|
11
|
+
throw new Error('Redis PII vault storage requires hGet/hSet/hGetAll or hget/hset/hgetall methods.');
|
|
12
|
+
}
|
|
13
|
+
const scopeKey = (scopeId) => `${keyPrefix}:scope:${scopeId}`;
|
|
14
|
+
async function maybeExpire(key) {
|
|
15
|
+
if (options.ttlSeconds && redis.expire) {
|
|
16
|
+
await redis.expire(key, options.ttlSeconds);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return createPiiVaultStorage({
|
|
20
|
+
async get(scopeId, token) {
|
|
21
|
+
return (await hGet(scopeKey(scopeId), token)) ?? undefined;
|
|
22
|
+
},
|
|
23
|
+
async getByToken(token) {
|
|
24
|
+
return (await hGet(tokenIndexKey, token)) ?? undefined;
|
|
25
|
+
},
|
|
26
|
+
async set(scopeId, token, value) {
|
|
27
|
+
const scopedKey = scopeKey(scopeId);
|
|
28
|
+
await hSet(scopedKey, token, value);
|
|
29
|
+
await hSet(tokenIndexKey, token, value);
|
|
30
|
+
await maybeExpire(scopedKey);
|
|
31
|
+
await maybeExpire(tokenIndexKey);
|
|
32
|
+
},
|
|
33
|
+
async entries(scopeId) {
|
|
34
|
+
const values = await hGetAll(scopeKey(scopeId));
|
|
35
|
+
return Object.entries(values).map(([token, value]) => ({ token, value }));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export class InMemoryPiiVaultStorage {
|
|
40
|
+
scopes = new Map();
|
|
41
|
+
tokenIndex = new Map();
|
|
42
|
+
get(scopeId, token) {
|
|
43
|
+
return this.scopes.get(scopeId)?.get(token);
|
|
44
|
+
}
|
|
45
|
+
getByToken(token) {
|
|
46
|
+
return this.tokenIndex.get(token);
|
|
47
|
+
}
|
|
48
|
+
set(scopeId, token, value) {
|
|
49
|
+
this.getScope(scopeId).set(token, value);
|
|
50
|
+
this.tokenIndex.set(token, value);
|
|
51
|
+
}
|
|
52
|
+
entries(scopeId) {
|
|
53
|
+
return Array.from(this.getScope(scopeId), ([token, value]) => ({ token, value }));
|
|
54
|
+
}
|
|
55
|
+
getScope(scopeId) {
|
|
56
|
+
let scope = this.scopes.get(scopeId);
|
|
57
|
+
if (!scope) {
|
|
58
|
+
scope = new Map();
|
|
59
|
+
this.scopes.set(scopeId, scope);
|
|
60
|
+
}
|
|
61
|
+
return scope;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export const defaultPiiVaultStorage = new InMemoryPiiVaultStorage();
|
package/dist/pii/tokenizer.d.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
+
import { type PiiVaultStorage } from './storage.js';
|
|
1
2
|
export type PiiResult = {
|
|
2
3
|
maskedText: string;
|
|
3
4
|
pii: Record<string, string>;
|
|
4
5
|
piiTypes: string[];
|
|
5
6
|
};
|
|
7
|
+
export type PiiTokenizerOptions = {
|
|
8
|
+
scopeId?: string;
|
|
9
|
+
storage?: PiiVaultStorage;
|
|
10
|
+
};
|
|
6
11
|
export declare class PiiTokenizer {
|
|
7
|
-
private
|
|
12
|
+
private valueToToken;
|
|
8
13
|
private counter;
|
|
9
14
|
private piiTypes;
|
|
15
|
+
private scopeId;
|
|
16
|
+
private tokenNamespace;
|
|
17
|
+
private storage;
|
|
18
|
+
constructor(options?: PiiTokenizerOptions);
|
|
10
19
|
private createToken;
|
|
11
20
|
mask(text: string, matches: {
|
|
12
21
|
type: string;
|
|
13
22
|
value: string;
|
|
14
|
-
}[]): PiiResult
|
|
15
|
-
unmask(text: string): string
|
|
16
|
-
|
|
23
|
+
}[]): Promise<PiiResult>;
|
|
24
|
+
unmask(text: string): Promise<string>;
|
|
25
|
+
importTokens(text: string): Promise<void>;
|
|
26
|
+
getVault(): Promise<{
|
|
17
27
|
[k: string]: string;
|
|
18
|
-
}
|
|
28
|
+
}>;
|
|
19
29
|
}
|
package/dist/pii/tokenizer.js
CHANGED
|
@@ -1,32 +1,69 @@
|
|
|
1
|
+
import { defaultPiiVaultStorage } from './storage.js';
|
|
1
2
|
export class PiiTokenizer {
|
|
2
|
-
|
|
3
|
+
valueToToken = new Map();
|
|
3
4
|
counter = 0;
|
|
4
5
|
piiTypes = new Set();
|
|
6
|
+
scopeId;
|
|
7
|
+
tokenNamespace;
|
|
8
|
+
storage;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.scopeId = options.scopeId ?? createVaultScopeId();
|
|
11
|
+
this.tokenNamespace = createVaultScopeId();
|
|
12
|
+
this.storage = options.storage ?? defaultPiiVaultStorage;
|
|
13
|
+
}
|
|
5
14
|
createToken(type) {
|
|
6
|
-
return `[[${type}_${this.counter++}]]`;
|
|
15
|
+
return `[[${type}_${this.tokenNamespace}_${this.counter++}]]`;
|
|
7
16
|
}
|
|
8
|
-
mask(text, matches) {
|
|
17
|
+
async mask(text, matches) {
|
|
9
18
|
let masked = text;
|
|
10
19
|
for (const match of matches) {
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
if (!masked.includes(match.value)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const key = `${match.type}:${match.value}`;
|
|
24
|
+
let token = this.valueToToken.get(key);
|
|
25
|
+
if (!token) {
|
|
26
|
+
token = this.createToken(match.type);
|
|
27
|
+
this.valueToToken.set(key, token);
|
|
28
|
+
await this.storage.set(this.scopeId, token, match.value);
|
|
29
|
+
}
|
|
13
30
|
this.piiTypes.add(match.type.toLowerCase());
|
|
14
31
|
masked = masked.split(match.value).join(token);
|
|
15
32
|
}
|
|
16
33
|
return {
|
|
17
34
|
maskedText: masked,
|
|
18
|
-
pii:
|
|
35
|
+
pii: await this.getVault(),
|
|
19
36
|
piiTypes: Array.from(this.piiTypes)
|
|
20
37
|
};
|
|
21
38
|
}
|
|
22
|
-
unmask(text) {
|
|
39
|
+
async unmask(text) {
|
|
23
40
|
let result = text;
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
const entries = await this.storage.entries(this.scopeId);
|
|
42
|
+
for (const { token, value } of entries) {
|
|
43
|
+
result = result.split(token).join(value);
|
|
44
|
+
}
|
|
27
45
|
return result;
|
|
28
46
|
}
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
async importTokens(text) {
|
|
48
|
+
if (!this.storage.getByToken) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const tokenMatches = Array.from(text.matchAll(/\[\[[A-Z_]+_[A-Za-z0-9]+_\d+\]\]/g));
|
|
52
|
+
for (const [token] of tokenMatches) {
|
|
53
|
+
const value = await this.storage.getByToken(token);
|
|
54
|
+
if (value) {
|
|
55
|
+
await this.storage.set(this.scopeId, token, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async getVault() {
|
|
60
|
+
const entries = await this.storage.entries(this.scopeId);
|
|
61
|
+
return Object.fromEntries(entries.map(({ token, value }) => [token, value]));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function createVaultScopeId() {
|
|
65
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
66
|
+
return crypto.randomUUID().replace(/-/g, '');
|
|
31
67
|
}
|
|
68
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
32
69
|
}
|
package/dist/util/singleton.js
CHANGED
|
@@ -21,7 +21,22 @@ export class ModelSingleton {
|
|
|
21
21
|
env.localModelPath = modelPath;
|
|
22
22
|
// Allow remote download if missing
|
|
23
23
|
env.allowRemoteModels = true;
|
|
24
|
-
console.log(
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
severityText: 'INFO',
|
|
27
|
+
severityNumber: 9,
|
|
28
|
+
body: 'Using guard model directory',
|
|
29
|
+
resource: {
|
|
30
|
+
attributes: {
|
|
31
|
+
'service.name': '@intflows/genkit-guard',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
attributes: {
|
|
35
|
+
'event.name': 'guard.models.directory',
|
|
36
|
+
'code.namespace': 'genkit-guard',
|
|
37
|
+
modelPath,
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
25
40
|
}
|
|
26
41
|
static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
27
42
|
if (!this.extractors.has(modelName)) {
|
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.11",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"prepare-models": "node scripts/download-model.js",
|
|
37
37
|
"build": "tsc",
|
|
38
|
+
"test": "npm run test:types && npm run test:storage && npm run test:concurrency",
|
|
39
|
+
"test:types": "tsc --noEmit --ignoreConfig --module NodeNext --moduleResolution NodeNext --target ESNext --strict --skipLibCheck scripts/test-types.ts",
|
|
40
|
+
"test:storage": "npm run build && node scripts/test-storage.js",
|
|
41
|
+
"test:concurrency": "npm run build && node scripts/test-concurrent-vault.js",
|
|
42
|
+
"test:redis": "npm run build && node scripts/test-redis-vault.js",
|
|
38
43
|
"prepublishOnly": "npm run build"
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
@@ -42,11 +47,11 @@
|
|
|
42
47
|
"zod": "^4.4.3"
|
|
43
48
|
},
|
|
44
49
|
"peerDependencies": {
|
|
45
|
-
"genkit": "
|
|
50
|
+
"genkit": "1.39.0"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@types/node": "^25.8.0",
|
|
49
|
-
"genkit": "^1.
|
|
54
|
+
"genkit": "^1.39.0",
|
|
50
55
|
"typescript": "^6.0.3"
|
|
51
56
|
}
|
|
52
57
|
}
|