@pratikw/detect 0.1.1

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,16 @@
1
+ import type { CloakResult, CustomPattern, DataCloakConfig, Detection, RestoreResult } from './types.js';
2
+ import { Vault } from './vault.js';
3
+ export declare class DataCloakEngine {
4
+ readonly vault: Vault;
5
+ private config;
6
+ constructor(config?: Partial<DataCloakConfig>);
7
+ detect(text: string): Detection[];
8
+ cloak(text: string): CloakResult;
9
+ private makeSynthetic;
10
+ private synthEnvValue;
11
+ private cloakInner;
12
+ private vaultImport;
13
+ private vaultAbsorb;
14
+ restore(text: string): RestoreResult;
15
+ }
16
+ export type { CustomPattern };
package/dist/engine.js ADDED
@@ -0,0 +1,108 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { defaultConfig } from './types.js';
3
+ import { detectPass1 } from './patterns/index.js';
4
+ import { scanEntropy } from './entropy.js';
5
+ import { synthesize } from './synthesizers/index.js';
6
+ import { synthesizeDsn } from './synthesizers/credentials.js';
7
+ import { opaqueToken } from './tokens.js';
8
+ import { Vault } from './vault.js';
9
+ export class DataCloakEngine {
10
+ vault;
11
+ config;
12
+ constructor(config = {}) {
13
+ this.config = { ...defaultConfig, ...config, detection: { ...defaultConfig.detection, ...config.detection }, vault: { ...defaultConfig.vault, ...config.vault } };
14
+ // Validate custom patterns loudly at construction (DET-08)
15
+ for (const c of this.config.customPatterns ?? []) {
16
+ try {
17
+ new RegExp(c.pattern);
18
+ }
19
+ catch {
20
+ throw new Error(`datacloak: invalid custom pattern "${c.name}"`);
21
+ }
22
+ }
23
+ this.vault = new Vault(this.config.vault.maxEntries);
24
+ }
25
+ detect(text) {
26
+ const d = this.config.detection;
27
+ const pass1 = detectPass1(text, { secrets: d.secrets, envVars: d.envVars, pii: d.pii }, this.config.customPatterns ?? []);
28
+ const out = [...pass1];
29
+ if (d.entropy) {
30
+ for (const h of scanEntropy(text, d.entropyThreshold)) {
31
+ if (out.some((x) => h.start < x.end && x.start < h.end))
32
+ continue;
33
+ out.push({ value: h.value, category: 'HIGH_ENTROPY_STRING', type: 'secret', start: h.start, end: h.end, confidence: 'medium' });
34
+ }
35
+ }
36
+ out.sort((a, b) => a.start - b.start || b.end - a.end);
37
+ return out;
38
+ }
39
+ cloak(text) {
40
+ const detections = this.detect(text);
41
+ const substitutions = [];
42
+ let result = text;
43
+ for (let i = detections.length - 1; i >= 0; i--) {
44
+ const det = detections[i];
45
+ const existing = this.vault.getByOriginal(det.value);
46
+ let synthetic = existing;
47
+ if (!synthetic) {
48
+ synthetic = this.makeSynthetic(det, text);
49
+ this.vault.set({ original: det.value, synthetic, category: det.category, type: det.type, synthesizedAt: Date.now(), confidence: det.confidence });
50
+ }
51
+ result = result.slice(0, det.start) + synthetic + result.slice(det.end);
52
+ substitutions.unshift({ original: det.value, synthetic, category: det.category });
53
+ }
54
+ return { text: result, substitutions };
55
+ }
56
+ makeSynthetic(det, fullText) {
57
+ if (det.category === 'ENV_VAR')
58
+ return this.synthEnvValue(det.value);
59
+ for (let attempt = 0; attempt < 3; attempt++) {
60
+ const s = synthesize(det.category, det.value) ?? opaqueToken(det.category);
61
+ if (!fullText.includes(s))
62
+ return s;
63
+ }
64
+ return opaqueToken(det.category);
65
+ }
66
+ synthEnvValue(value) {
67
+ const trimmed = value.replace(/^["']|["']$/g, '');
68
+ const quote = value.startsWith('"') || value.startsWith("'") ? value[0] : '';
69
+ const inner = this.cloakInner(trimmed);
70
+ return `${quote}${inner}${quote}`;
71
+ }
72
+ cloakInner(value) {
73
+ // DSN-aware: synthesize whole DSN so protocol/port survive
74
+ const dsnCats = ['DSN_POSTGRES', 'DSN_MONGO', 'DSN_REDIS', 'DSN_MYSQL', 'DSN_AMQP'];
75
+ const asDsn = detectPass1(value, { secrets: false, envVars: true, pii: false }).find((x) => dsnCats.includes(x.category));
76
+ if (asDsn && asDsn.value === value) {
77
+ const existing = this.vault.getByOriginal(value);
78
+ if (existing)
79
+ return existing;
80
+ const s = synthesizeDsn(value);
81
+ this.vault.set({ original: value, synthetic: s, category: asDsn.category, type: 'credential', synthesizedAt: Date.now(), confidence: 'high' });
82
+ return s;
83
+ }
84
+ // Otherwise cloak secrets/pii/entropy inside the value, offset by quote (handled by caller via full-string replace below)
85
+ const inner = new DataCloakEngine({ detection: { secrets: true, envVars: false, pii: true, entropy: true, entropyThreshold: this.config.detection.entropyThreshold }, vault: { maxEntries: this.config.vault.maxEntries } });
86
+ inner.vaultImport(this.vault);
87
+ const r = inner.cloak(value);
88
+ this.vaultAbsorb(inner.vault);
89
+ return r.text === value ? `SYNTH${faker.string.alphanumeric(16)}` : r.text;
90
+ }
91
+ vaultImport(_other) { }
92
+ vaultAbsorb(other) {
93
+ for (const e of other.list())
94
+ this.vault.set(e);
95
+ }
96
+ restore(text) {
97
+ const entries = this.vault.list().sort((a, b) => b.synthetic.length - a.synthetic.length);
98
+ let result = text;
99
+ let restored = 0;
100
+ for (const e of entries) {
101
+ if (!result.includes(e.synthetic))
102
+ continue;
103
+ result = result.split(e.synthetic).join(e.original);
104
+ restored++;
105
+ }
106
+ return { text: result, restored };
107
+ }
108
+ }
@@ -0,0 +1,6 @@
1
+ export declare function shannon(s: string): number;
2
+ export declare function scanEntropy(text: string, threshold: number): {
3
+ value: string;
4
+ start: number;
5
+ end: number;
6
+ }[];
@@ -0,0 +1,29 @@
1
+ export function shannon(s) {
2
+ if (s.length === 0)
3
+ return 0;
4
+ const freq = new Map();
5
+ for (const ch of s)
6
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
7
+ let h = 0;
8
+ for (const count of freq.values()) {
9
+ const p = count / s.length;
10
+ h -= p * Math.log2(p);
11
+ }
12
+ return h;
13
+ }
14
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15
+ export function scanEntropy(text, threshold) {
16
+ const out = [];
17
+ const re = /[^\s"'`,;()]+/g;
18
+ let m;
19
+ while ((m = re.exec(text)) !== null) {
20
+ const value = m[0];
21
+ if (value.length < 20 || UUID.test(value))
22
+ continue;
23
+ if (value.startsWith('data:image'))
24
+ continue;
25
+ if (shannon(value) >= threshold)
26
+ out.push({ value, start: m.index, end: m.index + value.length });
27
+ }
28
+ return out;
29
+ }
@@ -0,0 +1,4 @@
1
+ import type { PatternEntry } from './secrets.js';
2
+ export declare const CREDENTIAL_KEY_NAMES: string[];
3
+ export declare const isCredentialKey: (key: string) => boolean;
4
+ export declare const credentialPatterns: PatternEntry[];
@@ -0,0 +1,18 @@
1
+ export const CREDENTIAL_KEY_NAMES = ['PASSWORD', 'PASSWD', 'SECRET', 'API_KEY', 'APIKEY', 'API_SECRET', 'ACCESS_KEY', 'ACCESS_TOKEN', 'AUTH_TOKEN', 'PRIVATE_KEY', 'CLIENT_SECRET', 'DB_PASSWORD', 'DATABASE_URL', 'CONNECTION_STRING', 'AWS_SECRET_ACCESS_KEY', 'GITHUB_TOKEN', 'STRIPE_SECRET_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'SLACK_TOKEN', 'SENDGRID_API_KEY', 'TWILIO_AUTH_TOKEN', 'SUPABASE_KEY', 'VERCEL_TOKEN', 'SHOPIFY_TOKEN', 'GITLAB_TOKEN', 'HUGGINGFACE_TOKEN', 'JWT_SECRET', 'SESSION_SECRET', 'ENCRYPTION_KEY'];
2
+ export const isCredentialKey = (key) => {
3
+ const k = key.toUpperCase();
4
+ if (CREDENTIAL_KEY_NAMES.includes(k))
5
+ return true;
6
+ return /(PASSWORD|PASSWD|SECRET|TOKEN|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|API[_-]?KEY|CONNECTION|DATABASE[_-]?URL|DSN)/.test(k);
7
+ };
8
+ export const credentialPatterns = [
9
+ // Contextual matches first: on identical spans de-overlap keeps the earliest,
10
+ // so ENV_VAR wins over the bare DSN inside its value (test: env value span).
11
+ { name: 'env-var', category: 'ENV_VAR', type: 'credential', regex: /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*("[^"\n]*"|'[^'\n]*'|[^\s"'`#;]+)/gm, confidence: 'high', valueGroup: 2 },
12
+ { name: 'inline-config', category: 'INLINE_CONFIG', type: 'credential', regex: /["']?(?:api[_-]?key|secret|password|token|access[_-]?key)["']?\s*[:=]\s*["']([^"'`\s]+)["']?/gi, confidence: 'high', valueGroup: 1 },
13
+ { name: 'dsn-postgres', category: 'DSN_POSTGRES', type: 'credential', regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g, confidence: 'high' },
14
+ { name: 'dsn-mongo', category: 'DSN_MONGO', type: 'credential', regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g, confidence: 'high' },
15
+ { name: 'dsn-redis', category: 'DSN_REDIS', type: 'credential', regex: /redis:\/\/(?::[^\s@]+@)?[^\s"'`]+/g, confidence: 'high' },
16
+ { name: 'dsn-mysql', category: 'DSN_MYSQL', type: 'credential', regex: /mysql:\/\/[^\s"'`]+/g, confidence: 'high' },
17
+ { name: 'dsn-amqp', category: 'DSN_AMQP', type: 'credential', regex: /amqp:\/\/(?:[^:\s@]+(?::[^\s@]*)?@)?[^\s"'`]+/g, confidence: 'high' },
18
+ ];
@@ -0,0 +1,7 @@
1
+ import type { CustomPattern, Detection } from '../types.js';
2
+ export interface Pass1Opts {
3
+ secrets: boolean;
4
+ envVars: boolean;
5
+ pii: boolean;
6
+ }
7
+ export declare function detectPass1(text: string, opts: Pass1Opts, custom?: CustomPattern[]): Detection[];
@@ -0,0 +1,56 @@
1
+ import { secretPatterns } from './secrets.js';
2
+ import { credentialPatterns, isCredentialKey } from './credentials.js';
3
+ import { piiPatterns } from './pii.js';
4
+ export function detectPass1(text, opts, custom = []) {
5
+ const entries = [];
6
+ // Contextual credential patterns first: exact-tie de-overlap keeps the
7
+ // earliest, so ENV_VAR/INLINE_CONFIG win over the bare secret in their value.
8
+ if (opts.envVars)
9
+ entries.push(...credentialPatterns);
10
+ if (opts.secrets)
11
+ entries.push(...secretPatterns);
12
+ if (opts.pii)
13
+ entries.push(...piiPatterns);
14
+ for (const c of custom) {
15
+ let regex;
16
+ try {
17
+ regex = new RegExp(c.pattern, 'g');
18
+ }
19
+ catch {
20
+ throw new Error(`datacloak: invalid custom pattern "${c.name}"`);
21
+ }
22
+ entries.push({ name: c.name, category: c.category, type: c.type, regex, confidence: 'medium' });
23
+ }
24
+ const out = [];
25
+ for (const e of entries) {
26
+ e.regex.lastIndex = 0;
27
+ let m;
28
+ while ((m = e.regex.exec(text)) !== null) {
29
+ if (m[0].length === 0) {
30
+ e.regex.lastIndex++;
31
+ continue;
32
+ }
33
+ let value = m[0], start = m.index;
34
+ if (e.valueGroup !== undefined && m[e.valueGroup] !== undefined) {
35
+ value = m[e.valueGroup];
36
+ start = m.index + m[0].indexOf(value);
37
+ }
38
+ if (e.name === 'env-var') {
39
+ const key = m[1];
40
+ if (!isCredentialKey(key))
41
+ continue;
42
+ }
43
+ out.push({ value, category: e.category, type: e.type, start, end: start + value.length, confidence: e.confidence });
44
+ if (m[0].length === 0)
45
+ e.regex.lastIndex++;
46
+ }
47
+ }
48
+ out.sort((a, b) => a.start - b.start || b.end - a.end);
49
+ const kept = [];
50
+ for (const d of out) {
51
+ if (kept.length > 0 && d.start < kept[kept.length - 1].end)
52
+ continue;
53
+ kept.push(d);
54
+ }
55
+ return kept;
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { PatternEntry } from './secrets.js';
2
+ export declare const piiPatterns: PatternEntry[];
@@ -0,0 +1,7 @@
1
+ const p = (name, category, regex) => ({ name, category, type: 'pii', regex, confidence: 'high' });
2
+ export const piiPatterns = [
3
+ p('email', 'EMAIL', /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g),
4
+ p('phone-e164', 'PHONE_E164', /\+[1-9]\d{7,14}/g),
5
+ p('ipv4', 'IPV4', /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g),
6
+ p('phone-us', 'PHONE_US', /\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g),
7
+ ];
@@ -0,0 +1,10 @@
1
+ import type { EntryType, Confidence } from '../types.js';
2
+ export interface PatternEntry {
3
+ name: string;
4
+ category: string;
5
+ type: EntryType;
6
+ regex: RegExp;
7
+ confidence: Confidence;
8
+ valueGroup?: number;
9
+ }
10
+ export declare const secretPatterns: PatternEntry[];
@@ -0,0 +1,12 @@
1
+ const s = (name, category, regex) => ({ name, category, type: 'secret', regex, confidence: 'high' });
2
+ export const secretPatterns = [
3
+ s('anthropic', 'API_KEY_ANTHROPIC', /sk-ant-api03-[A-Za-z0-9\-_]{20,}/g),
4
+ s('openai', 'API_KEY_OPENAI', /sk-[A-Za-z0-9]{20,}/g),
5
+ s('aws', 'AWS_ACCESS_KEY', /AKIA[0-9A-Z]{16}/g),
6
+ s('github-classic', 'GITHUB_PAT', /ghp_[A-Za-z0-9]{36}/g),
7
+ s('github-fine', 'GITHUB_PAT', /github_pat_[A-Za-z0-9_]{22,}/g),
8
+ s('stripe-sk', 'STRIPE_KEY', /sk_live_[A-Za-z0-9]{16,}/g),
9
+ s('stripe-rk', 'STRIPE_KEY', /rk_live_[A-Za-z0-9]{16,}/g),
10
+ s('jwt', 'JWT', /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_=.]+/g),
11
+ s('pem', 'PEM_KEY', /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g),
12
+ ];
@@ -0,0 +1 @@
1
+ export declare function synthesizeDsn(original: string): string;
@@ -0,0 +1,12 @@
1
+ import { faker } from '@faker-js/faker';
2
+ const DSN = /^([a-z+]+:\/\/)(?:([^:@/\s]+)(?::([^@/\s]*))?@)?([^:/\s]+)(?::(\d+))?(\/[^?\s]*)?(\?[^\s]*)?$/i;
3
+ export function synthesizeDsn(original) {
4
+ const m = DSN.exec(original);
5
+ if (!m)
6
+ return original;
7
+ const [, proto, , , , port, path, query] = m;
8
+ const user = faker.internet.username().replace(/[^A-Za-z0-9_]/g, '_');
9
+ const pass = `SYNTHpw${faker.number.int({ min: 10, max: 99 })}`;
10
+ const host = `${faker.internet.domainWord()}.${faker.internet.domainSuffix()}`;
11
+ return `${proto}${user}:${pass}@${host}${port ? `:${port}` : ''}${path ?? ''}${query ?? ''}`;
12
+ }
@@ -0,0 +1 @@
1
+ export declare function synthesize(category: string, original: string): string | null;
@@ -0,0 +1,24 @@
1
+ import { synthEmail, synthIpv4, synthPhoneE164, synthPhoneUS } from './pii.js';
2
+ import { synthAnthropic, synthAws, synthGithub, synthJwt, synthOpenAI, synthPem, synthStripe } from './secrets.js';
3
+ import { synthesizeDsn } from './credentials.js';
4
+ export function synthesize(category, original) {
5
+ switch (category) {
6
+ case 'EMAIL': return synthEmail();
7
+ case 'PHONE_US': return synthPhoneUS();
8
+ case 'PHONE_E164': return synthPhoneE164();
9
+ case 'IPV4': return synthIpv4();
10
+ case 'API_KEY_OPENAI': return synthOpenAI();
11
+ case 'API_KEY_ANTHROPIC': return synthAnthropic();
12
+ case 'AWS_ACCESS_KEY': return synthAws();
13
+ case 'GITHUB_PAT': return synthGithub();
14
+ case 'STRIPE_KEY': return synthStripe(original.startsWith('rk_live_') ? 'rk_live_' : 'sk_live_');
15
+ case 'JWT': return synthJwt();
16
+ case 'PEM_KEY': return synthPem();
17
+ case 'DSN_POSTGRES':
18
+ case 'DSN_MONGO':
19
+ case 'DSN_REDIS':
20
+ case 'DSN_MYSQL':
21
+ case 'DSN_AMQP': return synthesizeDsn(original);
22
+ default: return null;
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ export declare function synthEmail(): string;
2
+ export declare function synthPhoneUS(): string;
3
+ export declare function synthPhoneE164(): string;
4
+ export declare function synthIpv4(): string;
@@ -0,0 +1,5 @@
1
+ import { faker } from '@faker-js/faker';
2
+ export function synthEmail() { return faker.internet.email(); }
3
+ export function synthPhoneUS() { return `(${faker.string.numeric(3)}) 555-01${faker.string.numeric(2)}`; }
4
+ export function synthPhoneE164() { return `+44770090${faker.string.numeric(4)}`; }
5
+ export function synthIpv4() { return faker.internet.ipv4(); }
@@ -0,0 +1,7 @@
1
+ export declare function synthOpenAI(): string;
2
+ export declare function synthAnthropic(): string;
3
+ export declare function synthAws(): string;
4
+ export declare function synthGithub(): string;
5
+ export declare function synthStripe(prefix: string): string;
6
+ export declare function synthJwt(overrides?: Record<string, unknown>): string;
7
+ export declare function synthPem(): string;
@@ -0,0 +1,29 @@
1
+ import { faker } from '@faker-js/faker';
2
+ const alnum = (n) => faker.string.alphanumeric(n);
3
+ export function synthOpenAI() { return `sk-SYNTH${alnum(20)}`; }
4
+ export function synthAnthropic() { return `sk-ant-api03-SYNTH${alnum(20)}`; }
5
+ export function synthAws() { return `AKIA${faker.string.alphanumeric({ length: 16, casing: 'upper' })}`; }
6
+ export function synthGithub() { return `ghp_SYNTH${alnum(31)}`; }
7
+ export function synthStripe(prefix) { return `${prefix}SYNTH${alnum(20)}`; }
8
+ export function synthJwt(overrides = {}) {
9
+ const b64 = (o) => {
10
+ const s = JSON.stringify(o);
11
+ const G = globalThis;
12
+ if (G.Buffer)
13
+ return G.Buffer.from(s, 'utf8').toString('base64url');
14
+ const bytes = new TextEncoder().encode(s);
15
+ let bin = '';
16
+ for (const b of bytes)
17
+ bin += String.fromCharCode(b);
18
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
19
+ };
20
+ const header = b64({ alg: 'HS256', typ: 'JWT' });
21
+ const now = Math.floor(Date.now() / 1000);
22
+ const body = b64({ sub: `synth-${faker.string.uuid()}`, iat: now, exp: now + 3600, ...overrides });
23
+ return `${header}.${body}.SYNTH${alnum(32)}`;
24
+ }
25
+ // ponytail: static throwaway PEM template; real @noble/curves keypair gen deferred to follow-up
26
+ export function synthPem() {
27
+ const b64body = faker.string.alphanumeric(64);
28
+ return `-----BEGIN RSA PRIVATE KEY-----\nSYNTH${b64body}\n-----END RSA PRIVATE KEY-----`;
29
+ }
@@ -0,0 +1 @@
1
+ export declare function opaqueToken(category: string): string;
package/dist/tokens.js ADDED
@@ -0,0 +1,4 @@
1
+ import { faker } from '@faker-js/faker';
2
+ export function opaqueToken(category) {
3
+ return `[${category.toUpperCase()}_${faker.string.alphanumeric({ length: 6, casing: 'upper' })}]`;
4
+ }
@@ -0,0 +1,51 @@
1
+ export type Confidence = 'high' | 'medium';
2
+ export type EntryType = 'pii' | 'secret' | 'credential';
3
+ export interface Detection {
4
+ value: string;
5
+ category: string;
6
+ type: EntryType;
7
+ start: number;
8
+ end: number;
9
+ confidence: Confidence;
10
+ }
11
+ export interface Substitution {
12
+ original: string;
13
+ synthetic: string;
14
+ category: string;
15
+ }
16
+ export interface CloakResult {
17
+ text: string;
18
+ substitutions: Substitution[];
19
+ }
20
+ export interface RestoreResult {
21
+ text: string;
22
+ restored: number;
23
+ }
24
+ export interface VaultEntry {
25
+ original: string;
26
+ synthetic: string;
27
+ category: string;
28
+ type: EntryType;
29
+ synthesizedAt: number;
30
+ confidence: Confidence;
31
+ }
32
+ export interface CustomPattern {
33
+ name: string;
34
+ pattern: string;
35
+ category: string;
36
+ type: EntryType;
37
+ }
38
+ export interface DataCloakConfig {
39
+ detection: {
40
+ secrets: boolean;
41
+ envVars: boolean;
42
+ pii: boolean;
43
+ entropy: boolean;
44
+ entropyThreshold: number;
45
+ };
46
+ vault: {
47
+ maxEntries: number;
48
+ };
49
+ customPatterns?: CustomPattern[];
50
+ }
51
+ export declare const defaultConfig: DataCloakConfig;
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ export const defaultConfig = {
2
+ detection: { secrets: true, envVars: true, pii: true, entropy: true, entropyThreshold: 4.5 },
3
+ vault: { maxEntries: 2000 },
4
+ };
@@ -0,0 +1,13 @@
1
+ import type { VaultEntry } from './types.js';
2
+ export declare class Vault {
3
+ private maxEntries;
4
+ private bySynthetic;
5
+ private origToSynth;
6
+ constructor(maxEntries?: number);
7
+ get size(): number;
8
+ set(entry: VaultEntry): void;
9
+ getBySynthetic(s: string): VaultEntry | undefined;
10
+ getByOriginal(o: string): string | undefined;
11
+ list(): VaultEntry[];
12
+ clear(): void;
13
+ }
package/dist/vault.js ADDED
@@ -0,0 +1,29 @@
1
+ export class Vault {
2
+ maxEntries;
3
+ bySynthetic = new Map();
4
+ origToSynth = new Map();
5
+ constructor(maxEntries = 2000) {
6
+ this.maxEntries = maxEntries;
7
+ }
8
+ get size() { return this.bySynthetic.size; }
9
+ set(entry) {
10
+ if (this.bySynthetic.has(entry.synthetic))
11
+ return;
12
+ const prev = this.origToSynth.get(entry.original);
13
+ if (prev !== undefined && prev !== entry.synthetic)
14
+ this.bySynthetic.delete(prev);
15
+ this.bySynthetic.set(entry.synthetic, entry);
16
+ this.origToSynth.set(entry.original, entry.synthetic);
17
+ while (this.bySynthetic.size > this.maxEntries) {
18
+ const oldest = this.bySynthetic.keys().next().value;
19
+ const evicted = this.bySynthetic.get(oldest);
20
+ this.bySynthetic.delete(oldest);
21
+ if (evicted && this.origToSynth.get(evicted.original) === oldest)
22
+ this.origToSynth.delete(evicted.original);
23
+ }
24
+ }
25
+ getBySynthetic(s) { return this.bySynthetic.get(s); }
26
+ getByOriginal(o) { return this.origToSynth.get(o); }
27
+ list() { return [...this.bySynthetic.values()]; }
28
+ clear() { this.bySynthetic.clear(); this.origToSynth.clear(); }
29
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@pratikw/detect",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "dist/engine.js",
6
+ "types": "dist/engine.d.ts",
7
+ "files": ["dist"],
8
+ "publishConfig": { "access": "public" },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/pratikwayal01/datacloak.git",
12
+ "directory": "packages/detect"
13
+ },
14
+ "scripts": { "build": "tsc", "test": "vitest run", "prepublishOnly": "npm run build && npm test" },
15
+ "dependencies": { "@faker-js/faker": "^10.6.0" },
16
+ "devDependencies": { "typescript": "~5.6.3", "vitest": "^3.0.0" }
17
+ }