@intflows/genkit-guard 0.0.10 → 0.0.12
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 +25 -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 +28 -0
- package/dist/pii/storage.js +95 -0
- package/package.json +11 -7
- package/scripts/test-redis-vault.js +153 -0
- package/scripts/test-storage.js +137 -0
- package/scripts/test-types.ts +21 -0
package/README.md
CHANGED
|
@@ -203,45 +203,48 @@ 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
|
+
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
213
|
|
|
213
214
|
```ts
|
|
214
|
-
import {
|
|
215
|
+
import { createClient } from "redis";
|
|
216
|
+
import { guard, createRedisPiiVaultStorage } from "@intflows/genkit-guard";
|
|
215
217
|
|
|
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
|
-
}
|
|
218
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
219
|
+
await redis.connect();
|
|
232
220
|
|
|
233
221
|
guard({
|
|
234
222
|
pii: {
|
|
235
223
|
reversible: true,
|
|
236
224
|
vault: {
|
|
237
|
-
storage:
|
|
225
|
+
storage: createRedisPiiVaultStorage(redis, {
|
|
226
|
+
keyPrefix: "my-app:pii",
|
|
227
|
+
ttlSeconds: 3600,
|
|
228
|
+
fallbackToMemory: true
|
|
229
|
+
}),
|
|
238
230
|
scopeId: (req, ctx) => ctx?.auth?.sessionId ?? req?.metadata?.requestId
|
|
239
231
|
}
|
|
240
232
|
}
|
|
241
233
|
});
|
|
242
234
|
```
|
|
243
235
|
|
|
244
|
-
|
|
236
|
+
`ttlSeconds` applies the configured expiry to both the scoped vault and token index. When
|
|
237
|
+
`fallbackToMemory` is enabled, successful writes are also mirrored in process memory and Redis
|
|
238
|
+
operation failures fall back to that mirror. The fallback is disabled by default, is local to one
|
|
239
|
+
process, and is not a replacement for Redis persistence or multi-worker availability. Its in-memory
|
|
240
|
+
entries observe the same TTL. Redis errors continue to propagate when fallback is disabled.
|
|
241
|
+
|
|
242
|
+
For another backend, use `createPiiVaultStorage({ get, set, entries, getByToken })` with your database, cache, or secret store.
|
|
243
|
+
|
|
244
|
+
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`.
|
|
245
|
+
|
|
246
|
+
### Screenshots
|
|
247
|
+

|
|
245
248
|
|
|
246
249
|
---
|
|
247
250
|
|
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,34 @@ 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
|
+
/** Expire Redis vault keys after this many seconds. Omit to keep them indefinitely. */
|
|
31
|
+
ttlSeconds?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Keep a process-local mirror and use it when a Redis operation fails.
|
|
34
|
+
* Disabled by default so Redis failures remain visible to callers.
|
|
35
|
+
*/
|
|
36
|
+
fallbackToMemory?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function createRedisPiiVaultStorage(redis: RedisPiiVaultClient, options?: RedisPiiVaultStorageOptions): PiiVaultStorage;
|
|
11
39
|
export declare class InMemoryPiiVaultStorage implements PiiVaultStorage {
|
|
12
40
|
private scopes;
|
|
13
41
|
private tokenIndex;
|
package/dist/pii/storage.js
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
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 ttlSeconds = options.ttlSeconds;
|
|
8
|
+
if (ttlSeconds !== undefined && (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
9
|
+
throw new Error('Redis PII vault ttlSeconds must be a positive integer.');
|
|
10
|
+
}
|
|
11
|
+
if (ttlSeconds !== undefined && !redis.expire) {
|
|
12
|
+
throw new Error('Redis PII vault ttlSeconds requires an expire method on the Redis client.');
|
|
13
|
+
}
|
|
14
|
+
const hGet = redis.hGet?.bind(redis) ?? redis.hget?.bind(redis);
|
|
15
|
+
const hSet = redis.hSet?.bind(redis) ?? redis.hset?.bind(redis);
|
|
16
|
+
const hGetAll = redis.hGetAll?.bind(redis) ?? redis.hgetall?.bind(redis);
|
|
17
|
+
if (!hGet || !hSet || !hGetAll) {
|
|
18
|
+
throw new Error('Redis PII vault storage requires hGet/hSet/hGetAll or hget/hset/hgetall methods.');
|
|
19
|
+
}
|
|
20
|
+
const scopeKey = (scopeId) => `${keyPrefix}:scope:${scopeId}`;
|
|
21
|
+
const fallback = options.fallbackToMemory ? new ExpiringInMemoryPiiVaultStorage(ttlSeconds) : undefined;
|
|
22
|
+
async function maybeExpire(key) {
|
|
23
|
+
if (ttlSeconds !== undefined) {
|
|
24
|
+
await redis.expire(key, ttlSeconds);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function withFallback(redisOperation, memoryOperation) {
|
|
28
|
+
try {
|
|
29
|
+
return await redisOperation();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!fallback)
|
|
33
|
+
throw error;
|
|
34
|
+
return memoryOperation();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return createPiiVaultStorage({
|
|
38
|
+
async get(scopeId, token) {
|
|
39
|
+
return withFallback(async () => (await hGet(scopeKey(scopeId), token)) ?? undefined, () => fallback.get(scopeId, token));
|
|
40
|
+
},
|
|
41
|
+
async getByToken(token) {
|
|
42
|
+
return withFallback(async () => (await hGet(tokenIndexKey, token)) ?? undefined, () => fallback.getByToken(token));
|
|
43
|
+
},
|
|
44
|
+
async set(scopeId, token, value) {
|
|
45
|
+
const scopedKey = scopeKey(scopeId);
|
|
46
|
+
// Warm the opt-in fallback on every write so data written before an outage is available.
|
|
47
|
+
await fallback?.set(scopeId, token, value);
|
|
48
|
+
await withFallback(async () => {
|
|
49
|
+
await hSet(scopedKey, token, value);
|
|
50
|
+
await hSet(tokenIndexKey, token, value);
|
|
51
|
+
await maybeExpire(scopedKey);
|
|
52
|
+
await maybeExpire(tokenIndexKey);
|
|
53
|
+
}, () => undefined);
|
|
54
|
+
},
|
|
55
|
+
async entries(scopeId) {
|
|
56
|
+
return withFallback(async () => {
|
|
57
|
+
const values = await hGetAll(scopeKey(scopeId));
|
|
58
|
+
return Object.entries(values).map(([token, value]) => ({ token, value }));
|
|
59
|
+
}, () => fallback.entries(scopeId));
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
class ExpiringInMemoryPiiVaultStorage {
|
|
64
|
+
ttlSeconds;
|
|
65
|
+
storage = new InMemoryPiiVaultStorage();
|
|
66
|
+
scopeExpiresAt = new Map();
|
|
67
|
+
tokenIndexExpiresAt;
|
|
68
|
+
constructor(ttlSeconds) {
|
|
69
|
+
this.ttlSeconds = ttlSeconds;
|
|
70
|
+
}
|
|
71
|
+
get(scopeId, token) {
|
|
72
|
+
return this.isScopeExpired(scopeId) ? undefined : this.storage.get(scopeId, token);
|
|
73
|
+
}
|
|
74
|
+
getByToken(token) {
|
|
75
|
+
return this.isTokenIndexExpired() ? undefined : this.storage.getByToken(token);
|
|
76
|
+
}
|
|
77
|
+
set(scopeId, token, value) {
|
|
78
|
+
this.storage.set(scopeId, token, value);
|
|
79
|
+
if (this.ttlSeconds !== undefined) {
|
|
80
|
+
const expiry = Date.now() + this.ttlSeconds * 1_000;
|
|
81
|
+
this.scopeExpiresAt.set(scopeId, expiry);
|
|
82
|
+
this.tokenIndexExpiresAt = expiry;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
entries(scopeId) {
|
|
86
|
+
return this.isScopeExpired(scopeId) ? [] : this.storage.entries(scopeId);
|
|
87
|
+
}
|
|
88
|
+
isScopeExpired(scopeId) {
|
|
89
|
+
const expiry = this.scopeExpiresAt.get(scopeId);
|
|
90
|
+
return expiry !== undefined && expiry <= Date.now();
|
|
91
|
+
}
|
|
92
|
+
isTokenIndexExpired() {
|
|
93
|
+
return this.tokenIndexExpiresAt !== undefined && this.tokenIndexExpiresAt <= Date.now();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
1
96
|
export class InMemoryPiiVaultStorage {
|
|
2
97
|
scopes = new Map();
|
|
3
98
|
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.12",
|
|
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,137 @@
|
|
|
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
|
+
assert.equal(redis.expirations.get('test:pii:scope:tenant:userA'), 60);
|
|
79
|
+
assert.equal(redis.expirations.get('test:pii:tokens'), 60);
|
|
80
|
+
|
|
81
|
+
class FailingRedis extends FakeRedis {
|
|
82
|
+
failed = false;
|
|
83
|
+
|
|
84
|
+
fail() {
|
|
85
|
+
this.failed = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async hGet(key, field) {
|
|
89
|
+
if (this.failed) throw new Error('Redis unavailable');
|
|
90
|
+
return super.hGet(key, field);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async hSet(key, field, value) {
|
|
94
|
+
if (this.failed) throw new Error('Redis unavailable');
|
|
95
|
+
return super.hSet(key, field, value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async hGetAll(key) {
|
|
99
|
+
if (this.failed) throw new Error('Redis unavailable');
|
|
100
|
+
return super.hGetAll(key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const failingRedis = new FailingRedis();
|
|
105
|
+
const resilientStorage = createRedisPiiVaultStorage(failingRedis, { fallbackToMemory: true });
|
|
106
|
+
await resilientStorage.set('resilient-scope', '[[EMAIL_resilient_0]]', 'safe@example.com');
|
|
107
|
+
failingRedis.fail();
|
|
108
|
+
assert.equal(
|
|
109
|
+
await resilientStorage.get('resilient-scope', '[[EMAIL_resilient_0]]'),
|
|
110
|
+
'safe@example.com'
|
|
111
|
+
);
|
|
112
|
+
assert.equal(await resilientStorage.getByToken('[[EMAIL_resilient_0]]'), 'safe@example.com');
|
|
113
|
+
assert.deepEqual(await resilientStorage.entries('resilient-scope'), [
|
|
114
|
+
{ token: '[[EMAIL_resilient_0]]', value: 'safe@example.com' },
|
|
115
|
+
]);
|
|
116
|
+
await resilientStorage.set('resilient-scope', '[[PHONE_resilient_1]]', '0400000000');
|
|
117
|
+
assert.equal(await resilientStorage.getByToken('[[PHONE_resilient_1]]'), '0400000000');
|
|
118
|
+
|
|
119
|
+
const strictRedis = new FailingRedis();
|
|
120
|
+
const strictStorage = createRedisPiiVaultStorage(strictRedis);
|
|
121
|
+
strictRedis.fail();
|
|
122
|
+
await assert.rejects(() => strictStorage.get('scope', 'token'), /Redis unavailable/);
|
|
123
|
+
|
|
124
|
+
assert.throws(
|
|
125
|
+
() => createRedisPiiVaultStorage(new FakeRedis(), { ttlSeconds: 0 }),
|
|
126
|
+
/positive integer/
|
|
127
|
+
);
|
|
128
|
+
assert.throws(
|
|
129
|
+
() =>
|
|
130
|
+
createRedisPiiVaultStorage(
|
|
131
|
+
{ hGet: async () => null, hSet: async () => undefined, hGetAll: async () => ({}) },
|
|
132
|
+
{ ttlSeconds: 60 }
|
|
133
|
+
),
|
|
134
|
+
/requires an expire method/
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
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
|
+
});
|