@intflows/genkit-guard 0.0.10 → 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 +17 -22
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/middleware/middleware.d.ts +20 -240
- package/dist/pii/storage.d.ts +22 -0
- package/dist/pii/storage.js +38 -0
- package/package.json +11 -7
- package/scripts/test-redis-vault.js +153 -0
- package/scripts/test-storage.js +78 -0
- package/scripts/test-types.ts +21 -0
package/README.md
CHANGED
|
@@ -203,45 +203,40 @@ pii: {
|
|
|
203
203
|
By default, PII is stored in an in-memory vault scoped to a single tokenizer instance. Tokens include a generated vault scope:
|
|
204
204
|
|
|
205
205
|
```txt
|
|
206
|
-
"Email john.doe@example.com" -> "Email [[EMAIL_<namespace>_0]]"
|
|
206
|
+
"Email john.doe@example.com" -> "Email [[EMAIL_<namespace>_0]]"
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
That generated namespace prevents two concurrent calls from sharing the same visible placeholder names. Vault lookups are isolated by the configured storage scope, so User A and User B can safely produce their own email tokens without cross-resolving each other's PII.
|
|
209
|
+
That generated namespace prevents two concurrent calls from sharing the same visible placeholder names. Vault lookups are isolated by the configured storage scope, so User A and User B can safely produce their own email tokens without cross-resolving each other's PII.
|
|
210
210
|
|
|
211
|
-
For applications that need persistence, distributed workers, audits, or tenant-specific storage, provide a vault storage backend:
|
|
211
|
+
For applications that need persistence, distributed workers, audits, or tenant-specific storage, provide a vault storage backend. Redis clients can be passed through the built-in helper:
|
|
212
212
|
|
|
213
213
|
```ts
|
|
214
|
-
import {
|
|
214
|
+
import { createClient } from "redis";
|
|
215
|
+
import { guard, createRedisPiiVaultStorage } from "@intflows/genkit-guard";
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
async get(scopeId: string, token: string) {
|
|
220
|
-
return this.redis.hget(`pii:${scopeId}`, token);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async set(scopeId: string, token: string, value: string) {
|
|
224
|
-
await this.redis.hset(`pii:${scopeId}`, token, value);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async entries(scopeId: string) {
|
|
228
|
-
const values = await this.redis.hgetall(`pii:${scopeId}`);
|
|
229
|
-
return Object.entries(values).map(([token, value]) => ({ token, value }));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
217
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
218
|
+
await redis.connect();
|
|
232
219
|
|
|
233
220
|
guard({
|
|
234
221
|
pii: {
|
|
235
222
|
reversible: true,
|
|
236
223
|
vault: {
|
|
237
|
-
storage:
|
|
224
|
+
storage: createRedisPiiVaultStorage(redis, {
|
|
225
|
+
keyPrefix: "my-app:pii",
|
|
226
|
+
ttlSeconds: 3600
|
|
227
|
+
}),
|
|
238
228
|
scopeId: (req, ctx) => ctx?.auth?.sessionId ?? req?.metadata?.requestId
|
|
239
229
|
}
|
|
240
230
|
}
|
|
241
231
|
});
|
|
242
232
|
```
|
|
243
233
|
|
|
244
|
-
|
|
234
|
+
For another backend, use `createPiiVaultStorage({ get, set, entries, getByToken })` with your database, cache, or secret store.
|
|
235
|
+
|
|
236
|
+
Choose a `scopeId` that matches your isolation boundary, such as request ID, session ID, tenant/user ID, or a combination like `tenantId:userId:requestId`. A shared external backend should never ignore `scopeId`, because placeholders are only safe when resolved against the correct vault scope. The placeholder sent to the model uses an opaque generated namespace rather than exposing your `scopeId`.
|
|
237
|
+
|
|
238
|
+
### Screenshots
|
|
239
|
+

|
|
245
240
|
|
|
246
241
|
---
|
|
247
242
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { guard, guardAction, guardMiddleware, guardPlugin } from './middleware/middleware.js';
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
2
|
+
export type { GuardConfig } from './middleware/middleware.js';
|
|
3
|
+
export { InMemoryPiiVaultStorage, createPiiVaultStorage, createRedisPiiVaultStorage, defaultPiiVaultStorage, } from './pii/storage.js';
|
|
4
|
+
export type { PiiVaultEntry, PiiVaultStorage, PiiVaultStorageAdapter, RedisPiiVaultClient, RedisPiiVaultStorageOptions, } from './pii/storage.js';
|
|
4
5
|
export * from './core/types.js';
|
|
5
6
|
/**
|
|
6
7
|
* Pre-load the model to avoid cold-start delay on first user request.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelSingleton } from './util/singleton.js';
|
|
2
2
|
// export { intentGuard, piiGuard } from './middleware/middleware.js';
|
|
3
3
|
export { guard, guardAction, guardMiddleware, guardPlugin } from './middleware/middleware.js';
|
|
4
|
-
export { InMemoryPiiVaultStorage, defaultPiiVaultStorage } from './pii/storage.js';
|
|
4
|
+
export { InMemoryPiiVaultStorage, createPiiVaultStorage, createRedisPiiVaultStorage, defaultPiiVaultStorage, } from './pii/storage.js';
|
|
5
5
|
export * from './core/types.js';
|
|
6
6
|
function logGuardEvent(eventName, body, attributes = {}) {
|
|
7
7
|
console.log(JSON.stringify({
|
|
@@ -1,254 +1,34 @@
|
|
|
1
1
|
import { z } from 'genkit';
|
|
2
2
|
import { type PiiVaultStorage } from '../pii/storage.js';
|
|
3
|
-
|
|
4
|
-
intent
|
|
5
|
-
mode
|
|
6
|
-
allowedIntent
|
|
7
|
-
semantic
|
|
8
|
-
threshold
|
|
9
|
-
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
10
|
-
}, "strip", z.ZodTypeAny, {
|
|
11
|
-
intents: Record<string, string>;
|
|
12
|
-
threshold?: number | undefined;
|
|
13
|
-
}, {
|
|
14
|
-
intents: Record<string, string>;
|
|
15
|
-
threshold?: number | undefined;
|
|
16
|
-
}>;
|
|
17
|
-
}, "strip", z.ZodTypeAny, {
|
|
18
|
-
semantic: {
|
|
19
|
-
intents: Record<string, string>;
|
|
20
|
-
threshold?: number | undefined;
|
|
21
|
-
};
|
|
22
|
-
mode?: string | undefined;
|
|
23
|
-
allowedIntent?: string | undefined;
|
|
24
|
-
}, {
|
|
25
|
-
semantic: {
|
|
26
|
-
intents: Record<string, string>;
|
|
27
|
-
threshold?: number | undefined;
|
|
28
|
-
};
|
|
29
|
-
mode?: string | undefined;
|
|
30
|
-
allowedIntent?: string | undefined;
|
|
31
|
-
}>>;
|
|
32
|
-
pii: z.ZodOptional<z.ZodObject<{
|
|
33
|
-
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
-
model: z.ZodOptional<z.ZodString>;
|
|
35
|
-
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
36
|
-
vault: z.ZodOptional<z.ZodObject<{
|
|
37
|
-
storage: z.ZodOptional<z.ZodAny>;
|
|
38
|
-
scopeId: z.ZodOptional<z.ZodAny>;
|
|
39
|
-
}, "strip", z.ZodTypeAny, {
|
|
40
|
-
storage?: any;
|
|
41
|
-
scopeId?: any;
|
|
42
|
-
}, {
|
|
43
|
-
storage?: any;
|
|
44
|
-
scopeId?: any;
|
|
45
|
-
}>>;
|
|
46
|
-
}, "strip", z.ZodTypeAny, {
|
|
47
|
-
mode?: "ner" | "classifier" | undefined;
|
|
48
|
-
reversible?: boolean | undefined;
|
|
49
|
-
model?: string | undefined;
|
|
50
|
-
vault?: {
|
|
51
|
-
storage?: any;
|
|
52
|
-
scopeId?: any;
|
|
53
|
-
} | undefined;
|
|
54
|
-
}, {
|
|
55
|
-
mode?: "ner" | "classifier" | undefined;
|
|
56
|
-
reversible?: boolean | undefined;
|
|
57
|
-
model?: string | undefined;
|
|
58
|
-
vault?: {
|
|
59
|
-
storage?: any;
|
|
60
|
-
scopeId?: any;
|
|
61
|
-
} | undefined;
|
|
62
|
-
}>>;
|
|
63
|
-
logging: z.ZodOptional<z.ZodObject<{
|
|
64
|
-
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
-
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
66
|
-
serviceName: z.ZodOptional<z.ZodString>;
|
|
67
|
-
}, "strip", z.ZodTypeAny, {
|
|
68
|
-
enabled?: boolean | undefined;
|
|
69
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
70
|
-
serviceName?: string | undefined;
|
|
71
|
-
}, {
|
|
72
|
-
enabled?: boolean | undefined;
|
|
73
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
74
|
-
serviceName?: string | undefined;
|
|
75
|
-
}>>;
|
|
76
|
-
models: z.ZodOptional<z.ZodObject<{
|
|
77
|
-
extractor: z.ZodOptional<z.ZodString>;
|
|
78
|
-
}, "strip", z.ZodTypeAny, {
|
|
79
|
-
extractor?: string | undefined;
|
|
80
|
-
}, {
|
|
81
|
-
extractor?: string | undefined;
|
|
82
|
-
}>>;
|
|
83
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
84
|
-
intent: z.ZodOptional<z.ZodObject<{
|
|
85
|
-
mode: z.ZodOptional<z.ZodString>;
|
|
86
|
-
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
87
|
-
semantic: z.ZodObject<{
|
|
88
|
-
threshold: z.ZodOptional<z.ZodNumber>;
|
|
89
|
-
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
90
|
-
}, "strip", z.ZodTypeAny, {
|
|
91
|
-
intents: Record<string, string>;
|
|
92
|
-
threshold?: number | undefined;
|
|
93
|
-
}, {
|
|
94
|
-
intents: Record<string, string>;
|
|
95
|
-
threshold?: number | undefined;
|
|
96
|
-
}>;
|
|
97
|
-
}, "strip", z.ZodTypeAny, {
|
|
98
|
-
semantic: {
|
|
99
|
-
intents: Record<string, string>;
|
|
100
|
-
threshold?: number | undefined;
|
|
101
|
-
};
|
|
102
|
-
mode?: string | undefined;
|
|
103
|
-
allowedIntent?: string | undefined;
|
|
104
|
-
}, {
|
|
105
|
-
semantic: {
|
|
106
|
-
intents: Record<string, string>;
|
|
107
|
-
threshold?: number | undefined;
|
|
108
|
-
};
|
|
109
|
-
mode?: string | undefined;
|
|
110
|
-
allowedIntent?: string | undefined;
|
|
111
|
-
}>>;
|
|
112
|
-
pii: z.ZodOptional<z.ZodObject<{
|
|
113
|
-
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
114
|
-
model: z.ZodOptional<z.ZodString>;
|
|
115
|
-
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
116
|
-
vault: z.ZodOptional<z.ZodObject<{
|
|
117
|
-
storage: z.ZodOptional<z.ZodAny>;
|
|
118
|
-
scopeId: z.ZodOptional<z.ZodAny>;
|
|
119
|
-
}, "strip", z.ZodTypeAny, {
|
|
120
|
-
storage?: any;
|
|
121
|
-
scopeId?: any;
|
|
122
|
-
}, {
|
|
123
|
-
storage?: any;
|
|
124
|
-
scopeId?: any;
|
|
125
|
-
}>>;
|
|
126
|
-
}, "strip", z.ZodTypeAny, {
|
|
127
|
-
mode?: "ner" | "classifier" | undefined;
|
|
128
|
-
reversible?: boolean | undefined;
|
|
129
|
-
model?: string | undefined;
|
|
130
|
-
vault?: {
|
|
131
|
-
storage?: any;
|
|
132
|
-
scopeId?: any;
|
|
133
|
-
} | undefined;
|
|
134
|
-
}, {
|
|
135
|
-
mode?: "ner" | "classifier" | undefined;
|
|
136
|
-
reversible?: boolean | undefined;
|
|
137
|
-
model?: string | undefined;
|
|
138
|
-
vault?: {
|
|
139
|
-
storage?: any;
|
|
140
|
-
scopeId?: any;
|
|
141
|
-
} | undefined;
|
|
142
|
-
}>>;
|
|
143
|
-
logging: z.ZodOptional<z.ZodObject<{
|
|
144
|
-
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
145
|
-
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
146
|
-
serviceName: z.ZodOptional<z.ZodString>;
|
|
147
|
-
}, "strip", z.ZodTypeAny, {
|
|
148
|
-
enabled?: boolean | undefined;
|
|
149
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
150
|
-
serviceName?: string | undefined;
|
|
151
|
-
}, {
|
|
152
|
-
enabled?: boolean | undefined;
|
|
153
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
154
|
-
serviceName?: string | undefined;
|
|
155
|
-
}>>;
|
|
156
|
-
models: z.ZodOptional<z.ZodObject<{
|
|
157
|
-
extractor: z.ZodOptional<z.ZodString>;
|
|
158
|
-
}, "strip", z.ZodTypeAny, {
|
|
159
|
-
extractor?: string | undefined;
|
|
160
|
-
}, {
|
|
161
|
-
extractor?: string | undefined;
|
|
162
|
-
}>>;
|
|
163
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
164
|
-
intent: z.ZodOptional<z.ZodObject<{
|
|
165
|
-
mode: z.ZodOptional<z.ZodString>;
|
|
166
|
-
allowedIntent: z.ZodOptional<z.ZodString>;
|
|
167
|
-
semantic: z.ZodObject<{
|
|
168
|
-
threshold: z.ZodOptional<z.ZodNumber>;
|
|
169
|
-
intents: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
170
|
-
}, "strip", z.ZodTypeAny, {
|
|
171
|
-
intents: Record<string, string>;
|
|
172
|
-
threshold?: number | undefined;
|
|
173
|
-
}, {
|
|
174
|
-
intents: Record<string, string>;
|
|
175
|
-
threshold?: number | undefined;
|
|
176
|
-
}>;
|
|
177
|
-
}, "strip", z.ZodTypeAny, {
|
|
178
|
-
semantic: {
|
|
179
|
-
intents: Record<string, string>;
|
|
180
|
-
threshold?: number | undefined;
|
|
181
|
-
};
|
|
182
|
-
mode?: string | undefined;
|
|
183
|
-
allowedIntent?: string | undefined;
|
|
184
|
-
}, {
|
|
185
|
-
semantic: {
|
|
3
|
+
export type GuardConfig = {
|
|
4
|
+
intent?: {
|
|
5
|
+
mode?: string;
|
|
6
|
+
allowedIntent?: string;
|
|
7
|
+
semantic?: {
|
|
8
|
+
threshold?: number;
|
|
186
9
|
intents: Record<string, string>;
|
|
187
|
-
threshold?: number | undefined;
|
|
188
10
|
};
|
|
189
|
-
|
|
190
|
-
allowedIntent?: string | undefined;
|
|
191
|
-
}>>;
|
|
192
|
-
pii: z.ZodOptional<z.ZodObject<{
|
|
193
|
-
reversible: z.ZodOptional<z.ZodBoolean>;
|
|
194
|
-
model: z.ZodOptional<z.ZodString>;
|
|
195
|
-
mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
|
|
196
|
-
vault: z.ZodOptional<z.ZodObject<{
|
|
197
|
-
storage: z.ZodOptional<z.ZodAny>;
|
|
198
|
-
scopeId: z.ZodOptional<z.ZodAny>;
|
|
199
|
-
}, "strip", z.ZodTypeAny, {
|
|
200
|
-
storage?: any;
|
|
201
|
-
scopeId?: any;
|
|
202
|
-
}, {
|
|
203
|
-
storage?: any;
|
|
204
|
-
scopeId?: any;
|
|
205
|
-
}>>;
|
|
206
|
-
}, "strip", z.ZodTypeAny, {
|
|
207
|
-
mode?: "ner" | "classifier" | undefined;
|
|
208
|
-
reversible?: boolean | undefined;
|
|
209
|
-
model?: string | undefined;
|
|
210
|
-
vault?: {
|
|
211
|
-
storage?: any;
|
|
212
|
-
scopeId?: any;
|
|
213
|
-
} | undefined;
|
|
214
|
-
}, {
|
|
215
|
-
mode?: "ner" | "classifier" | undefined;
|
|
216
|
-
reversible?: boolean | undefined;
|
|
217
|
-
model?: string | undefined;
|
|
218
|
-
vault?: {
|
|
219
|
-
storage?: any;
|
|
220
|
-
scopeId?: any;
|
|
221
|
-
} | undefined;
|
|
222
|
-
}>>;
|
|
223
|
-
logging: z.ZodOptional<z.ZodObject<{
|
|
224
|
-
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
225
|
-
level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
|
|
226
|
-
serviceName: z.ZodOptional<z.ZodString>;
|
|
227
|
-
}, "strip", z.ZodTypeAny, {
|
|
228
|
-
enabled?: boolean | undefined;
|
|
229
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
230
|
-
serviceName?: string | undefined;
|
|
231
|
-
}, {
|
|
232
|
-
enabled?: boolean | undefined;
|
|
233
|
-
level?: "debug" | "info" | "warn" | "error" | undefined;
|
|
234
|
-
serviceName?: string | undefined;
|
|
235
|
-
}>>;
|
|
236
|
-
models: z.ZodOptional<z.ZodObject<{
|
|
237
|
-
extractor: z.ZodOptional<z.ZodString>;
|
|
238
|
-
}, "strip", z.ZodTypeAny, {
|
|
239
|
-
extractor?: string | undefined;
|
|
240
|
-
}, {
|
|
241
|
-
extractor?: string | undefined;
|
|
242
|
-
}>>;
|
|
243
|
-
}, z.ZodTypeAny, "passthrough">>;
|
|
244
|
-
type GuardConfig = z.infer<typeof guardConfigSchema> & {
|
|
11
|
+
};
|
|
245
12
|
pii?: {
|
|
13
|
+
reversible?: boolean;
|
|
14
|
+
model?: string;
|
|
15
|
+
mode?: 'ner' | 'classifier';
|
|
246
16
|
vault?: {
|
|
247
17
|
storage?: PiiVaultStorage;
|
|
248
18
|
scopeId?: string | ((req: any, ctx: any) => string | undefined);
|
|
249
19
|
};
|
|
250
20
|
};
|
|
21
|
+
logging?: {
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
level?: LogSeverity;
|
|
24
|
+
serviceName?: string;
|
|
25
|
+
};
|
|
26
|
+
models?: {
|
|
27
|
+
extractor?: string;
|
|
28
|
+
};
|
|
29
|
+
[key: string]: any;
|
|
251
30
|
};
|
|
31
|
+
type LogSeverity = 'debug' | 'info' | 'warn' | 'error';
|
|
252
32
|
export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
|
|
253
33
|
intent: z.ZodOptional<z.ZodObject<{
|
|
254
34
|
mode: z.ZodOptional<z.ZodString>;
|
package/dist/pii/storage.d.ts
CHANGED
|
@@ -8,6 +8,28 @@ export interface PiiVaultStorage {
|
|
|
8
8
|
set(scopeId: string, token: string, value: string): void | Promise<void>;
|
|
9
9
|
entries(scopeId: string): PiiVaultEntry[] | Promise<PiiVaultEntry[]>;
|
|
10
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;
|
|
11
33
|
export declare class InMemoryPiiVaultStorage implements PiiVaultStorage {
|
|
12
34
|
private scopes;
|
|
13
35
|
private tokenIndex;
|
package/dist/pii/storage.js
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
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
|
+
}
|
|
1
39
|
export class InMemoryPiiVaultStorage {
|
|
2
40
|
scopes = new Map();
|
|
3
41
|
tokenIndex = new Map();
|
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",
|
|
@@ -32,12 +32,16 @@
|
|
|
32
32
|
"scripts/",
|
|
33
33
|
"dist/"
|
|
34
34
|
],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"prepare-models": "node scripts/download-model.js",
|
|
37
|
-
"build": "tsc",
|
|
38
|
-
"test
|
|
39
|
-
"
|
|
40
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepare-models": "node scripts/download-model.js",
|
|
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",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
41
45
|
"dependencies": {
|
|
42
46
|
"@huggingface/transformers": "^4.2.0",
|
|
43
47
|
"zod": "^4.4.3"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { createRedisPiiVaultStorage } from '../dist/index.js';
|
|
4
|
+
import { PiiTokenizer } from '../dist/pii/tokenizer.js';
|
|
5
|
+
|
|
6
|
+
class RespRedisClient {
|
|
7
|
+
constructor(host = '127.0.0.1', port = 6379) {
|
|
8
|
+
this.host = host;
|
|
9
|
+
this.port = port;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async hGet(key, field) {
|
|
13
|
+
return this.command('HGET', key, field);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async hSet(key, field, value) {
|
|
17
|
+
return this.command('HSET', key, field, value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async hGetAll(key) {
|
|
21
|
+
const values = await this.command('HGETALL', key);
|
|
22
|
+
const out = {};
|
|
23
|
+
for (let i = 0; i < values.length; i += 2) {
|
|
24
|
+
out[values[i]] = values[i + 1];
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async expire(key, seconds) {
|
|
30
|
+
return this.command('EXPIRE', key, String(seconds));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async del(key) {
|
|
34
|
+
return this.command('DEL', key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
command(...parts) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const socket = net.createConnection({ host: this.host, port: this.port });
|
|
40
|
+
let buffer = Buffer.alloc(0);
|
|
41
|
+
|
|
42
|
+
socket.setTimeout(3000);
|
|
43
|
+
socket.once('error', reject);
|
|
44
|
+
socket.once('timeout', () => {
|
|
45
|
+
socket.destroy();
|
|
46
|
+
reject(new Error(`Timed out connecting to Redis at ${this.host}:${this.port}`));
|
|
47
|
+
});
|
|
48
|
+
socket.once('connect', () => {
|
|
49
|
+
socket.write(encodeCommand(parts));
|
|
50
|
+
});
|
|
51
|
+
socket.on('data', (chunk) => {
|
|
52
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
53
|
+
try {
|
|
54
|
+
const [value, offset] = parseResp(buffer, 0);
|
|
55
|
+
if (offset <= buffer.length) {
|
|
56
|
+
socket.end();
|
|
57
|
+
resolve(value);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.message !== 'Incomplete RESP response') {
|
|
61
|
+
socket.destroy();
|
|
62
|
+
reject(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const redis = new RespRedisClient(process.env.REDIS_HOST ?? '127.0.0.1', Number(process.env.REDIS_PORT ?? 6379));
|
|
71
|
+
const keyPrefix = `genkit-guard:test:${Date.now()}`;
|
|
72
|
+
const storage = createRedisPiiVaultStorage(redis, { keyPrefix, ttlSeconds: 60 });
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const userA = new PiiTokenizer({ scopeId: 'tenant:userA', storage });
|
|
76
|
+
const userB = new PiiTokenizer({ scopeId: 'tenant:userB', storage });
|
|
77
|
+
|
|
78
|
+
const maskedA = await userA.mask('Email alice@example.com', [
|
|
79
|
+
{ type: 'EMAIL', value: 'alice@example.com' },
|
|
80
|
+
]);
|
|
81
|
+
const maskedB = await userB.mask('Email bob@example.com', [
|
|
82
|
+
{ type: 'EMAIL', value: 'bob@example.com' },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
assert.equal(await userA.unmask(maskedA.maskedText), 'Email alice@example.com');
|
|
86
|
+
assert.equal(await userB.unmask(maskedB.maskedText), 'Email bob@example.com');
|
|
87
|
+
assert.equal(await userA.unmask(maskedB.maskedText), maskedB.maskedText);
|
|
88
|
+
|
|
89
|
+
const laterPass = new PiiTokenizer({ scopeId: 'tenant:userA:later', storage });
|
|
90
|
+
await laterPass.importTokens(maskedA.maskedText);
|
|
91
|
+
assert.equal(await laterPass.unmask(maskedA.maskedText), 'Email alice@example.com');
|
|
92
|
+
|
|
93
|
+
console.log('Live Redis PII vault test passed.');
|
|
94
|
+
} finally {
|
|
95
|
+
await redis.del(`${keyPrefix}:tokens`).catch(() => {});
|
|
96
|
+
await redis.del(`${keyPrefix}:scope:tenant:userA`).catch(() => {});
|
|
97
|
+
await redis.del(`${keyPrefix}:scope:tenant:userB`).catch(() => {});
|
|
98
|
+
await redis.del(`${keyPrefix}:scope:tenant:userA:later`).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function encodeCommand(parts) {
|
|
102
|
+
return `*${parts.length}\r\n${parts.map((part) => {
|
|
103
|
+
const value = String(part);
|
|
104
|
+
return `$${Buffer.byteLength(value)}\r\n${value}\r\n`;
|
|
105
|
+
}).join('')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseResp(buffer, offset) {
|
|
109
|
+
if (offset >= buffer.length) {
|
|
110
|
+
throw new Error('Incomplete RESP response');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const prefix = String.fromCharCode(buffer[offset]);
|
|
114
|
+
if (prefix === '+') return parseSimple(buffer, offset);
|
|
115
|
+
if (prefix === '-') {
|
|
116
|
+
const [message] = parseSimple(buffer, offset);
|
|
117
|
+
throw new Error(message);
|
|
118
|
+
}
|
|
119
|
+
if (prefix === ':') {
|
|
120
|
+
const [value, next] = parseSimple(buffer, offset);
|
|
121
|
+
return [Number(value), next];
|
|
122
|
+
}
|
|
123
|
+
if (prefix === '$') return parseBulk(buffer, offset);
|
|
124
|
+
if (prefix === '*') return parseArray(buffer, offset);
|
|
125
|
+
throw new Error(`Unsupported RESP prefix: ${prefix}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseSimple(buffer, offset) {
|
|
129
|
+
const end = buffer.indexOf('\r\n', offset);
|
|
130
|
+
if (end === -1) throw new Error('Incomplete RESP response');
|
|
131
|
+
return [buffer.toString('utf8', offset + 1, end), end + 2];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseBulk(buffer, offset) {
|
|
135
|
+
const [lengthText, valueStart] = parseSimple(buffer, offset);
|
|
136
|
+
const length = Number(lengthText);
|
|
137
|
+
if (length === -1) return [null, valueStart];
|
|
138
|
+
const valueEnd = valueStart + length;
|
|
139
|
+
if (buffer.length < valueEnd + 2) throw new Error('Incomplete RESP response');
|
|
140
|
+
return [buffer.toString('utf8', valueStart, valueEnd), valueEnd + 2];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseArray(buffer, offset) {
|
|
144
|
+
const [lengthText, start] = parseSimple(buffer, offset);
|
|
145
|
+
const values = [];
|
|
146
|
+
let next = start;
|
|
147
|
+
for (let i = 0; i < Number(lengthText); i++) {
|
|
148
|
+
const [value, newOffset] = parseResp(buffer, next);
|
|
149
|
+
values.push(value);
|
|
150
|
+
next = newOffset;
|
|
151
|
+
}
|
|
152
|
+
return [values, next];
|
|
153
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
createPiiVaultStorage,
|
|
4
|
+
createRedisPiiVaultStorage,
|
|
5
|
+
InMemoryPiiVaultStorage,
|
|
6
|
+
} from '../dist/index.js';
|
|
7
|
+
import { PiiTokenizer } from '../dist/pii/tokenizer.js';
|
|
8
|
+
|
|
9
|
+
class FakeRedis {
|
|
10
|
+
hashes = new Map();
|
|
11
|
+
expirations = new Map();
|
|
12
|
+
|
|
13
|
+
async hGet(key, field) {
|
|
14
|
+
return this.hashes.get(key)?.[field] ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async hSet(key, field, value) {
|
|
18
|
+
const hash = this.hashes.get(key) ?? {};
|
|
19
|
+
hash[field] = value;
|
|
20
|
+
this.hashes.set(key, hash);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async hGetAll(key) {
|
|
24
|
+
return this.hashes.get(key) ?? {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async expire(key, seconds) {
|
|
28
|
+
this.expirations.set(key, seconds);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const memory = new InMemoryPiiVaultStorage();
|
|
33
|
+
await memory.set('scope-a', '[[EMAIL_token_0]]', 'a@example.com');
|
|
34
|
+
assert.equal(await memory.get('scope-a', '[[EMAIL_token_0]]'), 'a@example.com');
|
|
35
|
+
assert.equal(await memory.get('scope-b', '[[EMAIL_token_0]]'), undefined);
|
|
36
|
+
assert.equal(await memory.getByToken('[[EMAIL_token_0]]'), 'a@example.com');
|
|
37
|
+
|
|
38
|
+
const custom = createPiiVaultStorage({
|
|
39
|
+
async get(scopeId, token) {
|
|
40
|
+
return scopeId === 'custom' && token === '[[EMAIL_custom_0]]' ? 'custom@example.com' : undefined;
|
|
41
|
+
},
|
|
42
|
+
async set() {},
|
|
43
|
+
async entries(scopeId) {
|
|
44
|
+
return scopeId === 'custom'
|
|
45
|
+
? [{ token: '[[EMAIL_custom_0]]', value: 'custom@example.com' }]
|
|
46
|
+
: [];
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const customTokenizer = new PiiTokenizer({ scopeId: 'custom', storage: custom });
|
|
51
|
+
assert.equal(await customTokenizer.unmask('Hi [[EMAIL_custom_0]]'), 'Hi custom@example.com');
|
|
52
|
+
|
|
53
|
+
const redis = new FakeRedis();
|
|
54
|
+
const redisStorage = createRedisPiiVaultStorage(redis, {
|
|
55
|
+
keyPrefix: 'test:pii',
|
|
56
|
+
ttlSeconds: 60,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const userATokenizer = new PiiTokenizer({ scopeId: 'tenant:userA', storage: redisStorage });
|
|
60
|
+
const userBTokenizer = new PiiTokenizer({ scopeId: 'tenant:userB', storage: redisStorage });
|
|
61
|
+
|
|
62
|
+
const userAMasked = await userATokenizer.mask('Email alice@example.com', [
|
|
63
|
+
{ type: 'EMAIL', value: 'alice@example.com' },
|
|
64
|
+
]);
|
|
65
|
+
const userBMasked = await userBTokenizer.mask('Email bob@example.com', [
|
|
66
|
+
{ type: 'EMAIL', value: 'bob@example.com' },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
assert.notEqual(userAMasked.maskedText, userBMasked.maskedText);
|
|
70
|
+
assert.equal(await userATokenizer.unmask(userAMasked.maskedText), 'Email alice@example.com');
|
|
71
|
+
assert.equal(await userBTokenizer.unmask(userBMasked.maskedText), 'Email bob@example.com');
|
|
72
|
+
assert.equal(await userATokenizer.unmask(userBMasked.maskedText), userBMasked.maskedText);
|
|
73
|
+
|
|
74
|
+
const laterPassTokenizer = new PiiTokenizer({ scopeId: 'tenant:userA:later', storage: redisStorage });
|
|
75
|
+
await laterPassTokenizer.importTokens(userAMasked.maskedText);
|
|
76
|
+
assert.equal(await laterPassTokenizer.unmask(userAMasked.maskedText), 'Email alice@example.com');
|
|
77
|
+
|
|
78
|
+
console.log('PII vault storage tests passed.');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { guard, createRedisPiiVaultStorage, type RedisPiiVaultClient } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
const redis: RedisPiiVaultClient = {
|
|
4
|
+
async hGet() {
|
|
5
|
+
return undefined;
|
|
6
|
+
},
|
|
7
|
+
async hSet() {},
|
|
8
|
+
async hGetAll() {
|
|
9
|
+
return {};
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
guard({
|
|
14
|
+
pii: {
|
|
15
|
+
reversible: true,
|
|
16
|
+
vault: {
|
|
17
|
+
storage: createRedisPiiVaultStorage(redis),
|
|
18
|
+
scopeId: (req: any, ctx: any) => ctx?.auth?.sessionId ?? req?.metadata?.requestId,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|