@uniglot/wont-let-you-see 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -28,11 +28,11 @@ Configure via environment variables or JSON config file. Environment variables t
28
28
 
29
29
  #### Environment Variables
30
30
 
31
- | Variable | Description | Default |
32
- | ------------------------------------ | ----------------------------------------------- | ------- |
33
- | `WONT_LET_YOU_SEE_ENABLED` | Set to `false` or `0` to disable masking | `true` |
34
- | `WONT_LET_YOU_SEE_REVEALED_PATTERNS` | Comma-separated list of pattern types to reveal | (none) |
35
- | `WONT_LET_YOU_SEE_CUSTOM_PATTERNS` | Comma-separated list of custom strings to mask | (none) |
31
+ | Variable | Description | Default |
32
+ | ------------------------------------ | -------------------------------------------------------------------------- | ------- |
33
+ | `WONT_LET_YOU_SEE_ENABLED` | Set to `false` or `0` to disable masking | `true` |
34
+ | `WONT_LET_YOU_SEE_REVEALED_PATTERNS` | Comma-separated list of pattern types to reveal | (none) |
35
+ | `WONT_LET_YOU_SEE_CUSTOM_PATTERNS` | Comma-separated list of custom patterns to mask (supports `regex:` prefix) | (none) |
36
36
 
37
37
  #### JSON Config File
38
38
 
@@ -48,6 +48,18 @@ Create `.wont-let-you-see.json` in your project root, `~/.config/opencode/`, or
48
48
 
49
49
  > **Tip**: Add your AWS account ID to `customPatterns`. The built-in `account-id` pattern only matches contextual fields like `"OwnerId": "123456789012"`, but may miss bare account IDs in terraform output or other contexts.
50
50
 
51
+ Custom patterns support both literal strings and regular expressions. Prefix with `regex:` to use regex:
52
+
53
+ ```json
54
+ {
55
+ "customPatterns": [
56
+ "123456789012",
57
+ "my-secret-value",
58
+ "regex:secret-[a-z]{3}-\\d{4}"
59
+ ]
60
+ }
61
+ ```
62
+
51
63
  #### Examples
52
64
 
53
65
  ```bash
@@ -59,6 +71,9 @@ WONT_LET_YOU_SEE_REVEALED_PATTERNS=eks-cluster,ipv4 opencode
59
71
 
60
72
  # Mask custom values (e.g., AWS account ID)
61
73
  WONT_LET_YOU_SEE_CUSTOM_PATTERNS=123456789012,my-secret opencode
74
+
75
+ # Mask with regex patterns
76
+ WONT_LET_YOU_SEE_CUSTOM_PATTERNS="regex:token-[A-Z]{8},my-literal-secret" opencode
62
77
  ```
63
78
 
64
79
  ## Supported Commands
@@ -74,46 +89,53 @@ The plugin automatically masks output from:
74
89
 
75
90
  ### AWS Resources
76
91
 
77
- | Pattern Type | Description | Example |
78
- | ------------------ | ---------------------------- | ------------------------------------------------------- |
79
- | `arn` | Generic AWS ARNs | `arn:aws:iam::123456789012:user/admin` |
80
- | `eks-cluster` | EKS Cluster ARNs | `arn:aws:eks:us-west-2:123456789012:cluster/my-cluster` |
81
- | `account-id` | AWS Account IDs (contextual) | `"OwnerId": "123456789012"` |
82
- | `access-key-id` | AWS Access Key IDs | `AKIAIOSFODNN7EXAMPLE` |
83
- | `vpc` | VPC IDs | `vpc-0123456789abcdef0` |
84
- | `subnet` | Subnet IDs | `subnet-0123456789abcdef0` |
85
- | `security-group` | Security Group IDs | `sg-0123456789abcdef0` |
86
- | `internet-gateway` | Internet Gateway IDs | `igw-0123456789abcdef0` |
87
- | `route-table` | Route Table IDs | `rtb-0123456789abcdef0` |
88
- | `nat-gateway` | NAT Gateway IDs | `nat-0123456789abcdef0` |
89
- | `network-acl` | Network ACL IDs | `acl-0123456789abcdef0` |
90
- | `ec2-instance` | EC2 Instance IDs | `i-0123456789abcdef0` |
91
- | `ami` | AMI IDs | `ami-0123456789abcdef0` |
92
- | `ebs` | EBS Volume IDs | `vol-0123456789abcdef0` |
93
- | `snapshot` | EBS Snapshot IDs | `snap-0123456789abcdef0` |
94
- | `eni` | Network Interface IDs | `eni-0123456789abcdef0` |
95
- | `vpc-endpoint` | VPC Endpoint IDs | `vpce-0123456789abcdef0` |
96
- | `transit-gateway` | Transit Gateway IDs | `tgw-0123456789abcdef0` |
97
- | `customer-gateway` | Customer Gateway IDs | `cgw-0123456789abcdef0` |
98
- | `vpn-gateway` | VPN Gateway IDs | `vgw-0123456789abcdef0` |
99
- | `vpn-connection` | VPN Connection IDs | `vpn-0123456789abcdef0` |
100
- | `ecr-repo` | ECR Repository URIs | `123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo` |
92
+ | Pattern Type | Description | Example |
93
+ | ------------------- | ---------------------------- | ------------------------------------------------------- |
94
+ | `arn` | Generic AWS ARNs | `arn:aws:iam::123456789012:user/admin` |
95
+ | `eks-cluster` | EKS Cluster ARNs | `arn:aws:eks:us-west-2:123456789012:cluster/my-cluster` |
96
+ | `account-id` | AWS Account IDs (contextual) | `"OwnerId": "123456789012"` |
97
+ | `access-key-id` | AWS Access Key IDs | `AKIAIOSFODNN7EXAMPLE` |
98
+ | `secret-access-key` | AWS Secret Access Keys | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
99
+ | `vpc` | VPC IDs | `vpc-0123456789abcdef0` |
100
+ | `subnet` | Subnet IDs | `subnet-0123456789abcdef0` |
101
+ | `security-group` | Security Group IDs | `sg-0123456789abcdef0` |
102
+ | `internet-gateway` | Internet Gateway IDs | `igw-0123456789abcdef0` |
103
+ | `route-table` | Route Table IDs | `rtb-0123456789abcdef0` |
104
+ | `nat-gateway` | NAT Gateway IDs | `nat-0123456789abcdef0` |
105
+ | `network-acl` | Network ACL IDs | `acl-0123456789abcdef0` |
106
+ | `ec2-instance` | EC2 Instance IDs | `i-0123456789abcdef0` |
107
+ | `ami` | AMI IDs | `ami-0123456789abcdef0` |
108
+ | `ebs` | EBS Volume IDs | `vol-0123456789abcdef0` |
109
+ | `snapshot` | EBS Snapshot IDs | `snap-0123456789abcdef0` |
110
+ | `eni` | Network Interface IDs | `eni-0123456789abcdef0` |
111
+ | `vpc-endpoint` | VPC Endpoint IDs | `vpce-0123456789abcdef0` |
112
+ | `transit-gateway` | Transit Gateway IDs | `tgw-0123456789abcdef0` |
113
+ | `customer-gateway` | Customer Gateway IDs | `cgw-0123456789abcdef0` |
114
+ | `vpn-gateway` | VPN Gateway IDs | `vgw-0123456789abcdef0` |
115
+ | `vpn-connection` | VPN Connection IDs | `vpn-0123456789abcdef0` |
116
+ | `ecr-repo` | ECR Repository URIs | `123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo` |
101
117
 
102
118
  ### Kubernetes Resources (EKS)
103
119
 
104
- | Pattern Type | Description | Example |
105
- | -------------- | ---------------------------- | ---------------------------------- |
106
- | `k8s-token` | Service Account Tokens (JWT) | `eyJhbGciOiJSUzI1NiIs...` |
107
- | `k8s-endpoint` | EKS Cluster API Endpoints | `https://ABC123.eks.amazonaws.com` |
108
- | `k8s-node` | EKS Node Names | `ip-10-0-1-123.compute.internal` |
120
+ | Pattern Type | Description | Example |
121
+ | -------------- | ------------------------- | ---------------------------------- |
122
+ | `k8s-endpoint` | EKS Cluster API Endpoints | `https://ABC123.eks.amazonaws.com` |
123
+ | `k8s-node` | EKS Node Names | `ip-10-0-1-123.compute.internal` |
109
124
 
110
125
  ### Common Patterns
111
126
 
112
- | Pattern Type | Description | Example |
113
- | ------------- | --------------------------------------------- | ----------------------------------------------------------------- |
114
- | `ipv4` | IPv4 Addresses (including IP portion of CIDR) | `192.168.1.1`, `10.0.0.0/16` → `#(ipv4-1)/16` |
115
- | `private-key` | Private Key Blocks (entire key) | `-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----` |
116
- | `api-key` | API Keys (contextual) | `"api_key": "sk-..."` |
127
+ | Pattern Type | Description | Example |
128
+ | --------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
129
+ | `ipv4` | IPv4 Addresses (including IP portion of CIDR) | `192.168.1.1`, `10.0.0.0/16` → `#(ipv4-1)/16` |
130
+ | `private-key` | Private Key Blocks (entire key) | `-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----` |
131
+ | `api-key` | API Keys (contextual) | `"api_key": "sk-..."` |
132
+ | `phone-us` | US Phone Numbers | `+1-555-123-4567`, `(555) 123-4567` |
133
+ | `phone-kr` | South Korean Phone Numbers | `010-1234-5678`, `+82-10-1234-5678` |
134
+ | `phone-international` | International Phone Numbers | `+44 20 7946 0958` |
135
+ | `email` | Email Addresses | `user@example.com` |
136
+ | `uuid` | UUID/GUID | `550e8400-e29b-41d4-a716-446655440000` |
137
+ | `jwt` | JWT Tokens | `eyJhbGciOiJIUzI1NiIs...` |
138
+ | `base64-secret` | Base64-encoded Secrets (contextual) | `"secret": "SGVsbG8gV29ybGQ="` |
117
139
 
118
140
  ## Token Format
119
141
 
@@ -125,6 +147,10 @@ Examples:
125
147
  - `10.0.0.1` → `#(ipv4-1)`
126
148
  - `10.0.0.0/16` → `#(ipv4-1)/16` (subnet mask preserved)
127
149
  - `AKIAIOSFODNN7EXAMPLE` → `#(access-key-id-1)`
150
+ - `wJalrXUtnFEMI/K7MDENG...` → `#(secret-access-key-1)`
151
+ - `user@example.com` → `#(email-1)`
152
+ - `+1-555-123-4567` → `#(phone-us-1)`
153
+ - `eyJhbGciOiJIUzI1NiIs...` → `#(jwt-1)`
128
154
  - Entire private key block → `#(private-key-1)`
129
155
 
130
156
  ## How It Works
@@ -147,9 +173,49 @@ What was the actual VPC ID from the last command?
147
173
 
148
174
  The LLM should only know the token (e.g., `#(vpc-1)`), not the real value.
149
175
 
176
+ ## Contributing Patterns
177
+
178
+ Patterns are defined in JSON files under the `patterns/` directory:
179
+
180
+ - `patterns/aws.json` - AWS resource patterns
181
+ - `patterns/kubernetes.json` - Kubernetes patterns
182
+ - `patterns/common.json` - Common patterns (IPs, keys, etc.)
183
+
184
+ You can request to add new files for resources of different categories.
185
+
186
+ ### Pattern Format
187
+
188
+ Each pattern can be defined as:
189
+
190
+ ```json
191
+ {
192
+ "pattern-name": "^regex-pattern$",
193
+
194
+ "contextual-pattern": {
195
+ "pattern": "\"field\":\\s*\"(captured-value)\"",
196
+ "contextual": true
197
+ },
198
+
199
+ "literal-match": {
200
+ "exact": "literal-string-to-match"
201
+ }
202
+ }
203
+ ```
204
+
205
+ - **Simple regex**: Just a string with the regex pattern
206
+ - **Contextual**: For patterns where only a captured group should be masked (e.g., JSON fields)
207
+ - **Exact match**: For literal strings that should be escaped
208
+
209
+ ### Adding New Patterns
210
+
211
+ 1. Fork the repository
212
+ 2. Add your pattern to the appropriate JSON file
213
+ 3. Add tests in `src/__tests__/patterns.test.ts`
214
+ 4. Submit a pull request
215
+
150
216
  ## Limitations
151
217
 
152
- - **AWS only**: Currently supports AWS. GCP and Azure are not yet supported.
218
+ - **AWS only**: Currently supports AWS. GCP and Azure are not yet supported. Do you need them? Feel free to contribute!
153
219
  - **S3 Buckets**: Bucket names are not masked (often public/intentional).
154
220
  - **Account IDs**: Only masked in contextual JSON fields. Add to `customPatterns` for full coverage.
155
221
  - **UI display**: The UI shows original values (OpenCode limitation).
package/dist/index.js CHANGED
@@ -1,58 +1,82 @@
1
1
  // src/patterns.ts
2
- var AWS_PATTERNS = {
3
- arn: /^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9-]+:[a-z0-9-]*:(?:[0-9]{12})?:.+$/,
4
- eksCluster: /^arn:(?:aws|aws-cn|aws-us-gov):eks:[a-z0-9-]+:[0-9]{12}:cluster\/.+$/,
5
- vpc: /^vpc-[0-9a-f]{8,17}$/,
6
- subnet: /^subnet-[0-9a-f]{8,17}$/,
7
- securityGroup: /^sg-[0-9a-f]{8,17}$/,
8
- internetGateway: /^igw-[0-9a-f]{8,17}$/,
9
- routeTable: /^rtb-[0-9a-f]{8,17}$/,
10
- natGateway: /^nat-[0-9a-f]{8,17}$/,
11
- networkAcl: /^acl-[0-9a-f]{8,17}$/,
12
- ec2Instance: /^i-[0-9a-f]{8,17}$/,
13
- ami: /^ami-[0-9a-f]{8,17}$/,
14
- ebs: /^vol-[0-9a-f]{8,17}$/,
15
- snapshot: /^snap-[0-9a-f]{8,17}$/,
16
- eni: /^eni-[0-9a-f]{8,17}$/,
17
- vpcEndpoint: /^vpce-[0-9a-f]{8,17}$/,
18
- transitGateway: /^tgw-[0-9a-f]{8,17}$/,
19
- customerGateway: /^cgw-[0-9a-f]{8,17}$/,
20
- vpnGateway: /^vgw-[0-9a-f]{8,17}$/,
21
- vpnConnection: /^vpn-[0-9a-f]{8,17}$/,
22
- accountId: /"(?:OwnerId|AccountId|Owner|account_id)":\s*"(\d{12})"/,
23
- ecrRepoUri: /^(?:\d{12}|#\(custom-\d+\))\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com\/[a-z0-9._\/-]+$/,
24
- accessKeyId: /(?:^|[^A-Z0-9])(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}(?:[^A-Z0-9]|$)/
25
- };
26
- var K8S_PATTERNS = {
27
- serviceAccountToken: /^eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*$/,
28
- nodeNameAws: /^ip-(?:\d{1,3}-){3}\d{1,3}\.[a-z0-9-]+\.compute\.internal$/,
29
- clusterEndpoint: /^https:\/\/[A-Z0-9]+\.[a-z0-9-]+\.eks\.amazonaws\.com$/,
30
- kubeconfigServer: /^https:\/\/[0-9A-F]{32}\.[a-z]{2}-[a-z]+-\d\.eks\.amazonaws\.com$/i
31
- };
32
- var COMMON_PATTERNS = {
33
- ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
34
- privateKey: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
35
- apiKeyField: /"(?:api_key|apiKey|secret_key|secretKey|access_token|auth_token|password|token)":\s*"([^"]+)"/
36
- };
2
+ import { readdirSync, readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var cachedPatterns = null;
6
+ function getPackagePatternsDir() {
7
+ const currentFile = fileURLToPath(import.meta.url);
8
+ const srcDir = dirname(currentFile);
9
+ const packageRoot = dirname(srcDir);
10
+ return join(packageRoot, "patterns");
11
+ }
12
+ function parsePatternDefinition(name, definition) {
13
+ if (typeof definition === "string") {
14
+ return {
15
+ name,
16
+ pattern: new RegExp(definition),
17
+ isContextual: false,
18
+ isExact: false
19
+ };
20
+ }
21
+ if ("exact" in definition) {
22
+ const escaped = definition.exact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ return {
24
+ name,
25
+ pattern: new RegExp(escaped),
26
+ isContextual: false,
27
+ isExact: true
28
+ };
29
+ }
30
+ return {
31
+ name,
32
+ pattern: new RegExp(definition.pattern),
33
+ isContextual: definition.contextual ?? false,
34
+ isExact: false
35
+ };
36
+ }
37
+ function loadPatternFile(filePath) {
38
+ const content = readFileSync(filePath, "utf-8");
39
+ const data = JSON.parse(content);
40
+ const patterns = [];
41
+ for (const [name, definition] of Object.entries(data)) {
42
+ patterns.push(parsePatternDefinition(name, definition));
43
+ }
44
+ return patterns;
45
+ }
46
+ function loadPatterns() {
47
+ if (cachedPatterns) {
48
+ return cachedPatterns;
49
+ }
50
+ const patternsDir = getPackagePatternsDir();
51
+ const patterns = [];
52
+ const files = readdirSync(patternsDir).filter((f) => f.endsWith(".json")).sort();
53
+ for (const file of files) {
54
+ const filePath = join(patternsDir, file);
55
+ const filePatterns = loadPatternFile(filePath);
56
+ patterns.push(...filePatterns);
57
+ }
58
+ cachedPatterns = patterns;
59
+ return patterns;
60
+ }
37
61
 
38
62
  // src/mapping.ts
39
63
  import {
40
64
  existsSync,
41
65
  mkdirSync,
42
- readFileSync,
66
+ readFileSync as readFileSync2,
43
67
  writeFileSync,
44
68
  renameSync
45
69
  } from "fs";
46
- import { join } from "path";
70
+ import { join as join2 } from "path";
47
71
  import { homedir } from "os";
48
72
  var sessionStates = new Map;
49
73
  function getSessionPath(sessionId) {
50
74
  const projectRoot = process.cwd();
51
- const defaultPath = join(projectRoot, ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
52
- if (existsSync(join(projectRoot, ".opencode")) || !existsSync(homedir())) {
75
+ const defaultPath = join2(projectRoot, ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
76
+ if (existsSync(join2(projectRoot, ".opencode")) || !existsSync(homedir())) {
53
77
  return defaultPath;
54
78
  }
55
- return join(homedir(), ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
79
+ return join2(homedir(), ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
56
80
  }
57
81
  function ensureSessionDir(sessionPath) {
58
82
  const dir = sessionPath.substring(0, sessionPath.lastIndexOf("/"));
@@ -65,7 +89,7 @@ function getSessionState(sessionId) {
65
89
  if (!state) {
66
90
  const sessionPath = getSessionPath(sessionId);
67
91
  if (existsSync(sessionPath)) {
68
- const mapping = JSON.parse(readFileSync(sessionPath, "utf-8"));
92
+ const mapping = JSON.parse(readFileSync2(sessionPath, "utf-8"));
69
93
  const counters = {};
70
94
  for (const token of Object.keys(mapping.entries)) {
71
95
  const match = token.match(/^#\(([^-]+)-(\d+)\)$/);
@@ -118,7 +142,7 @@ import {
118
142
  existsSync as nodeExistsSync,
119
143
  readFileSync as nodeReadFileSync
120
144
  } from "fs";
121
- import { join as join2, dirname } from "path";
145
+ import { join as join3, dirname as dirname2 } from "path";
122
146
  import { homedir as homedir2 } from "os";
123
147
  var fsAdapter = {
124
148
  existsSync: nodeExistsSync,
@@ -134,14 +158,14 @@ function findConfigInAncestors(startDir) {
134
158
  const home = homedir2();
135
159
  let currentDir = startDir;
136
160
  while (true) {
137
- const configPath = join2(currentDir, CONFIG_FILENAME);
161
+ const configPath = join3(currentDir, CONFIG_FILENAME);
138
162
  if (fsAdapter.existsSync(configPath)) {
139
163
  return configPath;
140
164
  }
141
165
  if (currentDir === home) {
142
166
  break;
143
167
  }
144
- const parentDir = dirname(currentDir);
168
+ const parentDir = dirname2(currentDir);
145
169
  if (parentDir === currentDir) {
146
170
  break;
147
171
  }
@@ -157,7 +181,7 @@ function loadJsonConfig() {
157
181
  return JSON.parse(content);
158
182
  } catch {}
159
183
  }
160
- const homeConfig = join2(homedir2(), CONFIG_FILENAME);
184
+ const homeConfig = join3(homedir2(), CONFIG_FILENAME);
161
185
  if (fsAdapter.existsSync(homeConfig)) {
162
186
  try {
163
187
  const content = fsAdapter.readFileSync(homeConfig, "utf-8");
@@ -205,37 +229,6 @@ function isPatternEnabled(patternType) {
205
229
  }
206
230
 
207
231
  // src/masker.ts
208
- var PATTERN_ORDER = [
209
- { pattern: AWS_PATTERNS.eksCluster, type: "eks-cluster" },
210
- { pattern: AWS_PATTERNS.arn, type: "arn" },
211
- { pattern: AWS_PATTERNS.accountId, type: "account-id", isContextual: true },
212
- { pattern: AWS_PATTERNS.accessKeyId, type: "access-key-id" },
213
- { pattern: AWS_PATTERNS.vpc, type: "vpc" },
214
- { pattern: AWS_PATTERNS.subnet, type: "subnet" },
215
- { pattern: AWS_PATTERNS.securityGroup, type: "security-group" },
216
- { pattern: AWS_PATTERNS.internetGateway, type: "internet-gateway" },
217
- { pattern: AWS_PATTERNS.routeTable, type: "route-table" },
218
- { pattern: AWS_PATTERNS.natGateway, type: "nat-gateway" },
219
- { pattern: AWS_PATTERNS.networkAcl, type: "network-acl" },
220
- { pattern: AWS_PATTERNS.eni, type: "eni" },
221
- { pattern: AWS_PATTERNS.vpcEndpoint, type: "vpc-endpoint" },
222
- { pattern: AWS_PATTERNS.transitGateway, type: "transit-gateway" },
223
- { pattern: AWS_PATTERNS.customerGateway, type: "customer-gateway" },
224
- { pattern: AWS_PATTERNS.vpnGateway, type: "vpn-gateway" },
225
- { pattern: AWS_PATTERNS.vpnConnection, type: "vpn-connection" },
226
- { pattern: AWS_PATTERNS.ecrRepoUri, type: "ecr-repo" },
227
- { pattern: AWS_PATTERNS.ami, type: "ami" },
228
- { pattern: AWS_PATTERNS.ec2Instance, type: "ec2-instance" },
229
- { pattern: AWS_PATTERNS.ebs, type: "ebs" },
230
- { pattern: AWS_PATTERNS.snapshot, type: "snapshot" },
231
- { pattern: K8S_PATTERNS.serviceAccountToken, type: "k8s-token" },
232
- { pattern: K8S_PATTERNS.clusterEndpoint, type: "k8s-endpoint" },
233
- { pattern: K8S_PATTERNS.kubeconfigServer, type: "k8s-endpoint" },
234
- { pattern: K8S_PATTERNS.nodeNameAws, type: "k8s-node" },
235
- { pattern: COMMON_PATTERNS.privateKey, type: "private-key" },
236
- { pattern: COMMON_PATTERNS.apiKeyField, type: "api-key", isContextual: true },
237
- { pattern: COMMON_PATTERNS.ipv4, type: "ipv4" }
238
- ];
239
232
  function removeAnchors(source) {
240
233
  let result = source.replace(/^\^/, "").replace(/\$$/, "");
241
234
  if (!result.endsWith("\\b")) {
@@ -243,28 +236,42 @@ function removeAnchors(source) {
243
236
  }
244
237
  return result;
245
238
  }
246
- function mask(sessionId, text) {
247
- if (!text || typeof text !== "string") {
248
- return text;
249
- }
250
- const config = getConfig();
251
- if (!config.enabled) {
252
- return text;
253
- }
239
+ var REGEX_PREFIX = "regex:";
240
+ function applyCustomPatterns(sessionId, text, customPatterns) {
254
241
  let result = text;
255
- for (const customPattern of config.customPatterns) {
256
- if (result.includes(customPattern)) {
257
- const token = addEntry(sessionId, "custom", customPattern);
258
- result = result.split(customPattern).join(token);
242
+ for (const customPattern of customPatterns) {
243
+ if (customPattern.startsWith(REGEX_PREFIX)) {
244
+ const regexStr = customPattern.slice(REGEX_PREFIX.length);
245
+ const regex = new RegExp(removeAnchors(regexStr), "g");
246
+ const matches = new Set;
247
+ let match;
248
+ while ((match = regex.exec(result)) !== null) {
249
+ matches.add(match[0]);
250
+ }
251
+ for (const value of matches) {
252
+ const token = addEntry(sessionId, "custom", value);
253
+ result = result.split(value).join(token);
254
+ }
255
+ } else {
256
+ const escaped = customPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
257
+ const literalRegex = new RegExp(escaped + "(?![\\w])", "g");
258
+ if (literalRegex.test(result)) {
259
+ const token = addEntry(sessionId, "custom", customPattern);
260
+ result = result.replace(literalRegex, token);
261
+ }
259
262
  }
260
263
  }
261
- for (const { pattern, type, isContextual } of PATTERN_ORDER) {
262
- if (!isPatternEnabled(type)) {
264
+ return result;
265
+ }
266
+ function applyLoadedPatterns(sessionId, text, patterns) {
267
+ let result = text;
268
+ for (const { name, pattern, isContextual } of patterns) {
269
+ if (!isPatternEnabled(name)) {
263
270
  continue;
264
271
  }
265
272
  if (isContextual) {
266
273
  result = result.replace(new RegExp(pattern.source, "g"), (match, capturedValue) => {
267
- const token = addEntry(sessionId, type, capturedValue);
274
+ const token = addEntry(sessionId, name, capturedValue);
268
275
  return match.replace(capturedValue, token);
269
276
  });
270
277
  } else {
@@ -275,13 +282,27 @@ function mask(sessionId, text) {
275
282
  matches.add(match[0]);
276
283
  }
277
284
  for (const value of matches) {
278
- const token = addEntry(sessionId, type, value);
285
+ const token = addEntry(sessionId, name, value);
279
286
  result = result.split(value).join(token);
280
287
  }
281
288
  }
282
289
  }
283
290
  return result;
284
291
  }
292
+ function mask(sessionId, text) {
293
+ if (!text || typeof text !== "string") {
294
+ return text;
295
+ }
296
+ const config = getConfig();
297
+ if (!config.enabled) {
298
+ return text;
299
+ }
300
+ let result = text;
301
+ result = applyCustomPatterns(sessionId, result, config.customPatterns);
302
+ const patterns = loadPatterns();
303
+ result = applyLoadedPatterns(sessionId, result, patterns);
304
+ return result;
305
+ }
285
306
  function unmask(sessionId, text) {
286
307
  if (!text || typeof text !== "string") {
287
308
  return text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniglot/wont-let-you-see",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode plugin that masks sensitive cloud infrastructure data (AWS, Kubernetes) from LLMs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,28 @@
1
+ {
2
+ "eks-cluster": "^arn:(?:aws|aws-cn|aws-us-gov):eks:[a-z0-9-]+:[0-9]{12}:cluster\\/.+$",
3
+ "arn": "^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9-]+:[a-z0-9-]*:(?:[0-9]{12})?:.+$",
4
+ "account-id": {
5
+ "pattern": "\"(?:OwnerId|AccountId|Owner|account_id)\":\\s*\"(\\d{12})\"",
6
+ "contextual": true
7
+ },
8
+ "access-key-id": "(?:^|[^A-Z0-9])(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}(?:[^A-Z0-9]|$)",
9
+ "secret-access-key": "(?:^|[^A-Za-z0-9+/])[A-Za-z0-9+/]{40}(?:[^A-Za-z0-9+/]|$)",
10
+ "vpc": "^vpc-[0-9a-f]{8,17}$",
11
+ "subnet": "^subnet-[0-9a-f]{8,17}$",
12
+ "security-group": "^sg-[0-9a-f]{8,17}$",
13
+ "internet-gateway": "^igw-[0-9a-f]{8,17}$",
14
+ "route-table": "^rtb-[0-9a-f]{8,17}$",
15
+ "nat-gateway": "^nat-[0-9a-f]{8,17}$",
16
+ "network-acl": "^acl-[0-9a-f]{8,17}$",
17
+ "eni": "^eni-[0-9a-f]{8,17}$",
18
+ "vpc-endpoint": "^vpce-[0-9a-f]{8,17}$",
19
+ "transit-gateway": "^tgw-[0-9a-f]{8,17}$",
20
+ "customer-gateway": "^cgw-[0-9a-f]{8,17}$",
21
+ "vpn-gateway": "^vgw-[0-9a-f]{8,17}$",
22
+ "vpn-connection": "^vpn-[0-9a-f]{8,17}$",
23
+ "ecr-repo": "^(?:\\d{12}|#\\(custom-\\d+\\))\\.dkr\\.ecr\\.[a-z0-9-]+\\.amazonaws\\.com\\/[a-z0-9._\\/-]+$",
24
+ "ami": "^ami-[0-9a-f]{8,17}$",
25
+ "ec2-instance": "^i-[0-9a-f]{8,17}$",
26
+ "ebs": "^vol-[0-9a-f]{8,17}$",
27
+ "snapshot": "^snap-[0-9a-f]{8,17}$"
28
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "private-key": "-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
3
+ "api-key": {
4
+ "pattern": "\"(?:api_key|apiKey|secret_key|secretKey|access_token|auth_token|password|token)\":\\s*\"([^\"]+)\"",
5
+ "contextual": true
6
+ },
7
+ "ipv4": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
8
+ "phone-us": "^(?:\\+1[-\\s.]?)?(?:\\(?[2-9][0-9]{2}\\)?[-\\s.]?)?[2-9][0-9]{2}[-\\s.]?[0-9]{4}$",
9
+ "phone-kr": "^(?:\\+82[-\\s.]?|0)?(?:1[0-9]|2|3[1-3]|4[1-4]|5[1-5]|6[1-4])[-\\s.]?[0-9]{3,4}[-\\s.]?[0-9]{4}$",
10
+ "phone-international": "^\\+[1-9][0-9]{0,2}[-\\s.]?(?:[0-9][-\\s.]?){6,14}[0-9]$",
11
+ "email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
12
+ "uuid": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
13
+ "jwt": "^eyJ[A-Za-z0-9_-]*\\.eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*$",
14
+ "base64-secret": {
15
+ "pattern": "\"(?:secret|password|credential|private_key|privateKey|encryption_key|encryptionKey|signing_key|signingKey)\":\\s*\"([A-Za-z0-9+/]{32,}={0,2})\"",
16
+ "contextual": true
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "k8s-endpoint": "^https:\\/\\/[A-Z0-9]+\\.[a-z0-9-]+\\.eks\\.amazonaws\\.com$",
3
+ "k8s-endpoint-kubeconfig": {
4
+ "pattern": "^https:\\/\\/[0-9A-F]{32}\\.[a-z]{2}-[a-z]+-\\d\\.eks\\.amazonaws\\.com$",
5
+ "contextual": false
6
+ },
7
+ "k8s-node": "^ip-(?:\\d{1,3}-){3}\\d{1,3}\\.[a-z0-9-]+\\.compute\\.internal$"
8
+ }
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
2
  import { mask, unmask } from "../masker";
3
3
  import { createMapping } from "../mapping";
4
4
  import { resetConfig } from "../config";
5
+ import { resetPatternCache } from "../patterns";
5
6
 
6
7
  describe("masker", () => {
7
8
  const sessionId = "test-session-masker";
@@ -10,12 +11,14 @@ describe("masker", () => {
10
11
  beforeEach(() => {
11
12
  process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
12
13
  resetConfig();
14
+ resetPatternCache();
13
15
  createMapping(sessionId);
14
16
  });
15
17
 
16
18
  afterEach(() => {
17
19
  process.env = { ...originalEnv };
18
20
  resetConfig();
21
+ resetPatternCache();
19
22
  });
20
23
 
21
24
  describe("mask()", () => {
@@ -182,6 +185,67 @@ describe("masker", () => {
182
185
  });
183
186
  });
184
187
 
188
+ describe("customPatterns with regex support", () => {
189
+ it("should mask exact string patterns", () => {
190
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "my-secret-value";
191
+ resetConfig();
192
+
193
+ const input = "Secret: my-secret-value";
194
+ const result = mask(sessionId, input);
195
+
196
+ expect(result).toMatch(/Secret: #\(custom-\d+\)/);
197
+ expect(result).not.toContain("my-secret-value");
198
+ });
199
+
200
+ it("should mask regex patterns with regex: prefix", () => {
201
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
202
+ "regex:secret-[a-z]{3}-\\d{4}";
203
+ resetConfig();
204
+
205
+ const input = "Keys: secret-abc-1234, secret-xyz-5678";
206
+ const result = mask(sessionId, input);
207
+
208
+ expect(result).toMatch(/#\(custom-\d+\)/);
209
+ expect(result).not.toContain("secret-abc-1234");
210
+ expect(result).not.toContain("secret-xyz-5678");
211
+ });
212
+
213
+ it("should treat patterns without regex: prefix as literal strings", () => {
214
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "literal-value";
215
+ resetConfig();
216
+
217
+ const input = "Value: literal-value, Other: literal-values";
218
+ const result = mask(sessionId, input);
219
+
220
+ expect(result).toMatch(/Value: #\(custom-\d+\)/);
221
+ expect(result).toContain("literal-values");
222
+ });
223
+
224
+ it("should round-trip custom regex patterns", () => {
225
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "regex:token-[A-Z]{8}";
226
+ resetConfig();
227
+
228
+ const original = "Auth: token-ABCDEFGH";
229
+ const masked = mask(sessionId, original);
230
+ const unmasked = unmask(sessionId, masked);
231
+
232
+ expect(unmasked).toBe(original);
233
+ });
234
+
235
+ it("should support multiple custom patterns including regex", () => {
236
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
237
+ "exact-secret,regex:pattern-\\d+";
238
+ resetConfig();
239
+
240
+ const input = "Secrets: exact-secret, pattern-123, pattern-456";
241
+ const result = mask(sessionId, input);
242
+
243
+ expect(result).not.toContain("exact-secret");
244
+ expect(result).not.toContain("pattern-123");
245
+ expect(result).not.toContain("pattern-456");
246
+ });
247
+ });
248
+
185
249
  describe("round-trip integrity", () => {
186
250
  it("should maintain round-trip integrity for VPC", () => {
187
251
  const original = "vpc-1234567890abcdef0";