@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.
@@ -0,0 +1,96 @@
1
+ import assert from 'node:assert/strict';
2
+ import { InMemoryPiiVaultStorage } from '../dist/index.js';
3
+ import { PiiTokenizer } from '../dist/pii/tokenizer.js';
4
+
5
+ const storage = new InMemoryPiiVaultStorage();
6
+ const userCount = 100;
7
+
8
+ const users = Array.from({ length: userCount }, (_, index) => ({
9
+ scopeId: `tenantA:user${index}`,
10
+ email: `user${index}@example.com`,
11
+ }));
12
+
13
+ const results = await Promise.all(users.map(async ({ scopeId, email }) => {
14
+ const tokenizer = new PiiTokenizer({ scopeId, storage });
15
+ const prompt = `Fetch metadata for the blob named 'sensitive-user-data-${email}.json' inside Azure Blob Storage.`;
16
+ const masked = await tokenizer.mask(prompt, [{ type: 'EMAIL', value: email }]);
17
+
18
+ const simulatedModelToolInput = {
19
+ blobName: masked.maskedText.match(/'([^']+)'/)?.[1],
20
+ };
21
+
22
+ const toolInput = {
23
+ blobName: await tokenizer.unmask(simulatedModelToolInput.blobName),
24
+ };
25
+
26
+ const simulatedModelResponse = `The blob belongs to ${masked.maskedText.match(/\[\[EMAIL_[^\]]+\]\]/)?.[0]}.`;
27
+ const response = await tokenizer.unmask(simulatedModelResponse);
28
+
29
+ return {
30
+ scopeId,
31
+ email,
32
+ prompt,
33
+ maskedText: masked.maskedText,
34
+ toolInput,
35
+ response,
36
+ };
37
+ }));
38
+
39
+ const placeholders = new Set(results.map((result) => result.maskedText));
40
+ assert.equal(placeholders.size, userCount, 'each concurrent user should get a unique scoped placeholder');
41
+
42
+ for (const result of results) {
43
+ assert.equal(result.toolInput.blobName, `sensitive-user-data-${result.email}.json`);
44
+ assert.equal(result.response, `The blob belongs to ${result.email}.`);
45
+ }
46
+
47
+ const first = results[0];
48
+ const second = results[1];
49
+ const firstTokenizer = new PiiTokenizer({ scopeId: first.scopeId, storage });
50
+ assert.equal(
51
+ await firstTokenizer.unmask(second.maskedText),
52
+ second.maskedText,
53
+ 'one user scope must not resolve another user scope placeholder'
54
+ );
55
+
56
+ const promptTokenizer = new PiiTokenizer({ scopeId: 'trace:request', storage });
57
+ const promptMasked = await promptTokenizer.mask(
58
+ "Fetch metadata for the blob named 'sensitive-user-data-john.doe@example.com.json' inside Azure Blob Storage.",
59
+ [{ type: 'EMAIL', value: 'john.doe@example.com' }]
60
+ );
61
+
62
+ const toolResponseTokenizer = new PiiTokenizer({ scopeId: 'trace:request', storage });
63
+ await toolResponseTokenizer.mask(
64
+ 'john.doe@example.com',
65
+ [{ type: 'EMAIL', value: 'john.doe@example.com' }]
66
+ );
67
+
68
+ const finalModelResponse = `The metadata for the blob '${promptMasked.maskedText.match(/\[\[EMAIL_[^\]]+\]\]/)?.[0]}' has been successfully retrieved.`;
69
+ let unmaskedFinalResponse = finalModelResponse;
70
+ for (const tokenizer of [promptTokenizer, toolResponseTokenizer]) {
71
+ unmaskedFinalResponse = await tokenizer.unmask(unmaskedFinalResponse);
72
+ }
73
+
74
+ assert.equal(
75
+ unmaskedFinalResponse,
76
+ "The metadata for the blob 'john.doe@example.com' has been successfully retrieved.",
77
+ 'final model output should unmask placeholders created by earlier middleware passes'
78
+ );
79
+
80
+ const firstMiddlewarePass = new PiiTokenizer({ storage });
81
+ const firstPassMasked = await firstMiddlewarePass.mask(
82
+ "Fetch metadata for the blob named 'sensitive-user-data-jane.doe@example.com.json' inside Azure Blob Storage.",
83
+ [{ type: 'EMAIL', value: 'sensitive-user-data-jane.doe@example.com.json' }]
84
+ );
85
+ const laterMiddlewarePass = new PiiTokenizer({ storage });
86
+ const crossContextResponse = `The metadata for the blob '${firstPassMasked.maskedText.match(/\[\[EMAIL_[^\]]+\]\]/)?.[0]}' has been successfully retrieved.`;
87
+
88
+ await laterMiddlewarePass.importTokens(firstPassMasked.maskedText);
89
+
90
+ assert.equal(
91
+ await laterMiddlewarePass.unmask(crossContextResponse),
92
+ "The metadata for the blob 'sensitive-user-data-jane.doe@example.com.json' has been successfully retrieved.",
93
+ 'a later middleware context should resolve an earlier opaque token from the shared default vault'
94
+ );
95
+
96
+ console.log(`Concurrent PII vault simulation passed for ${userCount} isolated scopes.`);
@@ -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
+ });