@agentuity/cli 0.0.41 → 0.0.43

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.
Files changed (117) hide show
  1. package/bin/cli.ts +7 -5
  2. package/dist/banner.d.ts.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cmd/auth/index.d.ts.map +1 -1
  5. package/dist/cmd/auth/login.d.ts.map +1 -1
  6. package/dist/cmd/auth/whoami.d.ts +2 -0
  7. package/dist/cmd/auth/whoami.d.ts.map +1 -0
  8. package/dist/cmd/bundle/ast.d.ts +2 -0
  9. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  10. package/dist/cmd/bundle/index.d.ts +1 -1
  11. package/dist/cmd/bundle/index.d.ts.map +1 -1
  12. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  13. package/dist/cmd/cloud/deploy.d.ts +2 -0
  14. package/dist/cmd/cloud/deploy.d.ts.map +1 -0
  15. package/dist/cmd/cloud/index.d.ts +2 -0
  16. package/dist/cmd/cloud/index.d.ts.map +1 -0
  17. package/dist/cmd/dev/index.d.ts.map +1 -1
  18. package/dist/cmd/env/delete.d.ts +2 -0
  19. package/dist/cmd/env/delete.d.ts.map +1 -0
  20. package/dist/cmd/env/get.d.ts +2 -0
  21. package/dist/cmd/env/get.d.ts.map +1 -0
  22. package/dist/cmd/env/import.d.ts +2 -0
  23. package/dist/cmd/env/import.d.ts.map +1 -0
  24. package/dist/cmd/env/index.d.ts +2 -0
  25. package/dist/cmd/env/index.d.ts.map +1 -0
  26. package/dist/cmd/env/list.d.ts +2 -0
  27. package/dist/cmd/env/list.d.ts.map +1 -0
  28. package/dist/cmd/env/pull.d.ts +2 -0
  29. package/dist/cmd/env/pull.d.ts.map +1 -0
  30. package/dist/cmd/env/push.d.ts +2 -0
  31. package/dist/cmd/env/push.d.ts.map +1 -0
  32. package/dist/cmd/env/set.d.ts +2 -0
  33. package/dist/cmd/env/set.d.ts.map +1 -0
  34. package/dist/cmd/project/delete.d.ts.map +1 -1
  35. package/dist/cmd/project/download.d.ts +1 -1
  36. package/dist/cmd/project/download.d.ts.map +1 -1
  37. package/dist/cmd/project/list.d.ts.map +1 -1
  38. package/dist/cmd/project/show.d.ts.map +1 -1
  39. package/dist/cmd/project/template-flow.d.ts +1 -1
  40. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  41. package/dist/cmd/secret/delete.d.ts +2 -0
  42. package/dist/cmd/secret/delete.d.ts.map +1 -0
  43. package/dist/cmd/secret/get.d.ts +2 -0
  44. package/dist/cmd/secret/get.d.ts.map +1 -0
  45. package/dist/cmd/secret/import.d.ts +2 -0
  46. package/dist/cmd/secret/import.d.ts.map +1 -0
  47. package/dist/cmd/secret/index.d.ts +2 -0
  48. package/dist/cmd/secret/index.d.ts.map +1 -0
  49. package/dist/cmd/secret/list.d.ts +2 -0
  50. package/dist/cmd/secret/list.d.ts.map +1 -0
  51. package/dist/cmd/secret/pull.d.ts +2 -0
  52. package/dist/cmd/secret/pull.d.ts.map +1 -0
  53. package/dist/cmd/secret/push.d.ts +2 -0
  54. package/dist/cmd/secret/push.d.ts.map +1 -0
  55. package/dist/cmd/secret/set.d.ts +2 -0
  56. package/dist/cmd/secret/set.d.ts.map +1 -0
  57. package/dist/cmd/version/index.d.ts.map +1 -1
  58. package/dist/config.d.ts +4 -1
  59. package/dist/config.d.ts.map +1 -1
  60. package/dist/env-util.d.ts +67 -0
  61. package/dist/env-util.d.ts.map +1 -0
  62. package/dist/env-util.test.d.ts +2 -0
  63. package/dist/env-util.test.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/schema-parser.d.ts.map +1 -1
  67. package/dist/steps.d.ts.map +1 -1
  68. package/dist/tui.d.ts +1 -1
  69. package/dist/tui.d.ts.map +1 -1
  70. package/dist/types.d.ts +35 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/banner.ts +7 -2
  74. package/src/cli.ts +46 -5
  75. package/src/cmd/auth/index.ts +2 -1
  76. package/src/cmd/auth/login.ts +2 -4
  77. package/src/cmd/auth/whoami.ts +69 -0
  78. package/src/cmd/bundle/ast.ts +169 -4
  79. package/src/cmd/bundle/index.ts +2 -2
  80. package/src/cmd/bundle/plugin.ts +42 -1
  81. package/src/cmd/cloud/deploy.ts +129 -0
  82. package/src/cmd/cloud/index.ts +8 -0
  83. package/src/cmd/dev/index.ts +93 -9
  84. package/src/cmd/env/delete.ts +62 -0
  85. package/src/cmd/env/get.ts +66 -0
  86. package/src/cmd/env/import.ts +117 -0
  87. package/src/cmd/env/index.ts +22 -0
  88. package/src/cmd/env/list.ts +69 -0
  89. package/src/cmd/env/pull.ts +93 -0
  90. package/src/cmd/env/push.ts +55 -0
  91. package/src/cmd/env/set.ts +86 -0
  92. package/src/cmd/project/create.ts +1 -1
  93. package/src/cmd/project/delete.ts +43 -2
  94. package/src/cmd/project/download.ts +1 -1
  95. package/src/cmd/project/list.ts +33 -2
  96. package/src/cmd/project/show.ts +35 -3
  97. package/src/cmd/project/template-flow.ts +53 -12
  98. package/src/cmd/secret/delete.ts +55 -0
  99. package/src/cmd/secret/get.ts +67 -0
  100. package/src/cmd/secret/import.ts +79 -0
  101. package/src/cmd/secret/index.ts +22 -0
  102. package/src/cmd/secret/list.ts +69 -0
  103. package/src/cmd/secret/pull.ts +91 -0
  104. package/src/cmd/secret/push.ts +55 -0
  105. package/src/cmd/secret/set.ts +60 -0
  106. package/src/cmd/version/index.ts +2 -1
  107. package/src/config.ts +60 -7
  108. package/src/env-util.test.ts +194 -0
  109. package/src/env-util.ts +290 -0
  110. package/src/index.ts +5 -1
  111. package/src/schema-parser.ts +2 -3
  112. package/src/steps.ts +79 -4
  113. package/src/tui.ts +92 -56
  114. package/src/types.ts +30 -1
  115. package/dist/logger.d.ts +0 -24
  116. package/dist/logger.d.ts.map +0 -1
  117. package/src/logger.ts +0 -235
@@ -0,0 +1,91 @@
1
+ import { z } from 'zod';
2
+ import { join } from 'node:path';
3
+ import { createSubcommand } from '../../types';
4
+ import * as tui from '../../tui';
5
+ import { projectGet } from '@agentuity/server';
6
+ import { getAPIBaseURL, APIClient } from '../../api';
7
+ import { loadProjectConfig } from '../../config';
8
+ import {
9
+ findEnvFile,
10
+ findExistingEnvFile,
11
+ readEnvFile,
12
+ writeEnvFile,
13
+ mergeEnvVars,
14
+ } from '../../env-util';
15
+
16
+ export const pullSubcommand = createSubcommand({
17
+ name: 'pull',
18
+ description: 'Pull secrets from cloud to local .env.production file',
19
+ requiresAuth: true,
20
+ schema: {
21
+ options: z.object({
22
+ dir: z.string().optional().describe('project directory (default: current directory)'),
23
+ force: z.boolean().default(false).describe('overwrite local values with cloud values'),
24
+ }),
25
+ },
26
+
27
+ async handler(ctx) {
28
+ const { opts, config } = ctx;
29
+ const dir = opts?.dir ?? process.cwd();
30
+
31
+ // Load project config to get project ID
32
+ const projectConfig = await loadProjectConfig(dir);
33
+ if (!projectConfig) {
34
+ tui.fatal(`No Agentuity project found in ${dir}. Missing agentuity.json`);
35
+ }
36
+
37
+ const apiUrl = getAPIBaseURL(config);
38
+ const client = new APIClient(apiUrl, config);
39
+
40
+ // Fetch project with unmasked secrets
41
+ const project = await tui.spinner('Pulling secrets from cloud', () => {
42
+ return projectGet(client, { id: projectConfig.projectId, mask: false });
43
+ });
44
+
45
+ const cloudSecrets = project.secrets || {};
46
+
47
+ // Read current local env from existing file (.env.production or .env)
48
+ const existingEnvPath = await findExistingEnvFile(dir);
49
+ const localEnv = await readEnvFile(existingEnvPath);
50
+
51
+ // Target file is always .env.production
52
+ const targetEnvPath = await findEnvFile(dir);
53
+
54
+ // Merge: cloud values override local if force=true, otherwise keep local
55
+ let mergedEnv: Record<string, string>;
56
+ if (opts?.force) {
57
+ // Cloud values take priority
58
+ mergedEnv = mergeEnvVars(localEnv, cloudSecrets);
59
+ } else {
60
+ // Local values take priority (only add new keys from cloud)
61
+ mergedEnv = mergeEnvVars(cloudSecrets, localEnv);
62
+ }
63
+
64
+ // Write to .env.production (skip AGENTUITY_ keys)
65
+ await writeEnvFile(targetEnvPath, mergedEnv, {
66
+ skipKeys: Object.keys(mergedEnv).filter((k) => k.startsWith('AGENTUITY_')),
67
+ });
68
+
69
+ // Write AGENTUITY_SDK_KEY to .env if present and missing locally
70
+ if (project.api_key) {
71
+ const dotEnvPath = join(dir, '.env');
72
+ const dotEnv = await readEnvFile(dotEnvPath);
73
+
74
+ if (!dotEnv.AGENTUITY_SDK_KEY) {
75
+ dotEnv.AGENTUITY_SDK_KEY = project.api_key;
76
+ await writeEnvFile(dotEnvPath, dotEnv, {
77
+ addComment: (key) => {
78
+ if (key === 'AGENTUITY_SDK_KEY') {
79
+ return 'AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
80
+ }
81
+ return null;
82
+ },
83
+ });
84
+ tui.info(`Wrote AGENTUITY_SDK_KEY to ${dotEnvPath}`);
85
+ }
86
+ }
87
+
88
+ const count = Object.keys(cloudSecrets).length;
89
+ tui.success(`Pulled ${count} secret${count !== 1 ? 's' : ''} to ${targetEnvPath}`);
90
+ },
91
+ });
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand } from '../../types';
3
+ import * as tui from '../../tui';
4
+ import { projectEnvUpdate } from '@agentuity/server';
5
+ import { getAPIBaseURL, APIClient } from '../../api';
6
+ import { loadProjectConfig } from '../../config';
7
+ import { findEnvFile, readEnvFile, filterAgentuitySdkKeys } from '../../env-util';
8
+
9
+ export const pushSubcommand = createSubcommand({
10
+ name: 'push',
11
+ description: 'Push secrets from local .env.production file to cloud',
12
+ requiresAuth: true,
13
+ schema: {
14
+ options: z.object({
15
+ dir: z.string().optional().describe('project directory (default: current directory)'),
16
+ }),
17
+ },
18
+
19
+ async handler(ctx) {
20
+ const { opts, config } = ctx;
21
+ const dir = opts?.dir ?? process.cwd();
22
+
23
+ // Load project config to get project ID
24
+ const projectConfig = await loadProjectConfig(dir);
25
+ if (!projectConfig) {
26
+ tui.fatal(`No Agentuity project found in ${dir}. Missing agentuity.json`);
27
+ }
28
+
29
+ // Read local env file
30
+ const envFilePath = await findEnvFile(dir);
31
+ const localEnv = await readEnvFile(envFilePath);
32
+
33
+ // Filter out AGENTUITY_ prefixed keys (don't push SDK keys)
34
+ const filteredSecrets = filterAgentuitySdkKeys(localEnv);
35
+
36
+ if (Object.keys(filteredSecrets).length === 0) {
37
+ tui.warning('No secrets to push');
38
+ return;
39
+ }
40
+
41
+ const apiUrl = getAPIBaseURL(config);
42
+ const client = new APIClient(apiUrl, config);
43
+
44
+ // Push to cloud (using secrets field)
45
+ await tui.spinner('Pushing secrets to cloud', () => {
46
+ return projectEnvUpdate(client, {
47
+ id: projectConfig.projectId,
48
+ secrets: filteredSecrets,
49
+ });
50
+ });
51
+
52
+ const count = Object.keys(filteredSecrets).length;
53
+ tui.success(`Pushed ${count} secret${count !== 1 ? 's' : ''} to cloud`);
54
+ },
55
+ });
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand } from '../../types';
3
+ import * as tui from '../../tui';
4
+ import { projectEnvUpdate } from '@agentuity/server';
5
+ import { getAPIBaseURL, APIClient } from '../../api';
6
+ import { loadProjectConfig } from '../../config';
7
+ import { findEnvFile, readEnvFile, writeEnvFile, filterAgentuitySdkKeys } from '../../env-util';
8
+
9
+ export const setSubcommand = createSubcommand({
10
+ name: 'set',
11
+ description: 'Set a secret',
12
+ requiresAuth: true,
13
+ schema: {
14
+ args: z.object({
15
+ key: z.string().min(1, 'key must not be empty').describe('the secret key'),
16
+ value: z.string().min(1, 'value must not be empty').describe('the secret value'),
17
+ }),
18
+ options: z.object({
19
+ dir: z.string().optional().describe('project directory (default: current directory)'),
20
+ }),
21
+ },
22
+
23
+ async handler(ctx) {
24
+ const { args, opts, config } = ctx;
25
+ const dir = opts?.dir ?? process.cwd();
26
+
27
+ // Validate key doesn't start with AGENTUITY_
28
+ if (args.key.startsWith('AGENTUITY_')) {
29
+ tui.fatal('Cannot set AGENTUITY_ prefixed variables. These are reserved for system use.');
30
+ }
31
+
32
+ // Load project config to get project ID
33
+ const projectConfig = await loadProjectConfig(dir);
34
+ if (!projectConfig) {
35
+ tui.fatal(`No Agentuity project found in ${dir}. Missing agentuity.json`);
36
+ }
37
+
38
+ const apiUrl = getAPIBaseURL(config);
39
+ const client = new APIClient(apiUrl, config);
40
+
41
+ // Set in cloud (using secrets field)
42
+ await tui.spinner('Setting secret in cloud', () => {
43
+ return projectEnvUpdate(client, {
44
+ id: projectConfig.projectId,
45
+ secrets: { [args.key]: args.value },
46
+ });
47
+ });
48
+
49
+ // Update local .env.production file
50
+ const envFilePath = await findEnvFile(dir);
51
+ const currentEnv = await readEnvFile(envFilePath);
52
+ currentEnv[args.key] = args.value;
53
+
54
+ // Filter out AGENTUITY_ keys before writing
55
+ const filteredEnv = filterAgentuitySdkKeys(currentEnv);
56
+ await writeEnvFile(envFilePath, filteredEnv);
57
+
58
+ tui.success(`Secret '${args.key}' set successfully (cloud + ${envFilePath})`);
59
+ },
60
+ });
@@ -1,6 +1,6 @@
1
1
  import { createCommand } from '../../types';
2
2
  import { getVersion } from '../../version';
3
- import { logger } from '../../logger';
3
+ import { createLogger } from '@agentuity/server';
4
4
 
5
5
  export const command = createCommand({
6
6
  name: 'version',
@@ -10,6 +10,7 @@ export const command = createCommand({
10
10
  try {
11
11
  console.log(getVersion());
12
12
  } catch (error) {
13
+ const logger = createLogger();
13
14
  logger.fatal('Failed to retrieve version: %s', error);
14
15
  }
15
16
  },
package/src/config.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { YAML } from 'bun';
2
- import { join, extname, basename } from 'node:path';
2
+ import { join, extname, basename, resolve, normalize } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { mkdir, readdir, readFile, writeFile, chmod } from 'node:fs/promises';
5
5
  import type { Config, Profile, AuthData } from './types';
6
- import { ConfigSchema, ProjectSchema } from './types';
6
+ import { ConfigSchema, ProjectSchema, BuildMetadataSchema, type BuildMetadata } from './types';
7
7
  import * as tui from './tui';
8
8
  import { z } from 'zod';
9
9
 
@@ -189,8 +189,18 @@ export async function saveConfig(config: Config, customPath?: string): Promise<v
189
189
  await chmod(configPath, 0o600);
190
190
  }
191
191
 
192
+ async function getOrInitConfig(): Promise<Config> {
193
+ const config = await loadConfig();
194
+ if (config) {
195
+ return config;
196
+ }
197
+ const profilePath = await getProfile();
198
+ const name = basename(profilePath, '.yaml');
199
+ return { name };
200
+ }
201
+
192
202
  export async function saveAuth(auth: AuthData): Promise<void> {
193
- const config = (await loadConfig()) || { name: 'default' };
203
+ const config = await getOrInitConfig();
194
204
  config.auth = {
195
205
  api_key: auth.apiKey,
196
206
  user_id: auth.userId,
@@ -202,7 +212,7 @@ export async function saveAuth(auth: AuthData): Promise<void> {
202
212
  }
203
213
 
204
214
  export async function clearAuth(): Promise<void> {
205
- const config = (await loadConfig()) || { name: 'default' };
215
+ const config = await getOrInitConfig();
206
216
  config.auth = {
207
217
  api_key: '',
208
218
  user_id: '',
@@ -213,6 +223,21 @@ export async function clearAuth(): Promise<void> {
213
223
  await saveConfig(config);
214
224
  }
215
225
 
226
+ export async function saveProjectDir(projectDir: string): Promise<void> {
227
+ const config = await getOrInitConfig();
228
+ config.preferences = config.preferences || {};
229
+ const normalized = resolve(normalize(projectDir));
230
+ (config.preferences as Record<string, unknown>).project_dir = normalized;
231
+ await saveConfig(config);
232
+ }
233
+
234
+ export async function saveOrgId(orgId: string): Promise<void> {
235
+ const config = await getOrInitConfig();
236
+ config.preferences = config.preferences || {};
237
+ (config.preferences as Record<string, unknown>).orgId = orgId;
238
+ await saveConfig(config);
239
+ }
240
+
216
241
  export async function getAuth(): Promise<AuthData | null> {
217
242
  const config = await loadConfig();
218
243
  if (!config) return null;
@@ -350,11 +375,39 @@ type InitialProjectConfig = ProjectConfig & {
350
375
  };
351
376
 
352
377
  export async function createProjectConfig(dir: string, config: InitialProjectConfig) {
378
+ // Create a sanitized config without the apiKey for agentuity.json
379
+ const { apiKey, ...sanitizedConfig } = config;
380
+
353
381
  const configPath = join(dir, 'agentuity.json');
354
382
  const configFile = Bun.file(configPath);
355
- await configFile.write(JSON.stringify(config, null, 2));
383
+ await configFile.write(JSON.stringify(sanitizedConfig, null, 2));
356
384
 
385
+ // Write SDK key to .env with comment
357
386
  const envPath = join(dir, '.env');
358
- const envFile = Bun.file(envPath);
359
- await envFile.write(`AGENTUITY_SDK_KEY=${config.apiKey}`);
387
+ const comment =
388
+ '# AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
389
+ const content = `${comment}\nAGENTUITY_SDK_KEY=${apiKey}\n`;
390
+ await Bun.write(envPath, content);
391
+ // Set restrictive permissions (owner read/write only) to protect sensitive key
392
+ await chmod(envPath, 0o600);
393
+ }
394
+
395
+ export async function loadBuildMetadata(dir: string): Promise<BuildMetadata> {
396
+ const filename = join(dir, 'agentuity.metadata.json');
397
+ const file = Bun.file(filename);
398
+ if (!(await file.exists())) {
399
+ throw new Error(`couldn't find ${filename}`);
400
+ }
401
+ const buffer = await file.text();
402
+ const config = JSON.parse(buffer);
403
+ const result = BuildMetadataSchema.safeParse(config);
404
+ if (!result.success) {
405
+ tui.error(`Invalid build metadata at ${filename}:`);
406
+ for (const issue of result.error.issues) {
407
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
408
+ tui.bullet(`${path}: ${issue.message}`);
409
+ }
410
+ process.exit(1);
411
+ }
412
+ return result.data;
360
413
  }
@@ -0,0 +1,194 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { looksLikeSecret } from './env-util';
3
+
4
+ describe('looksLikeSecret', () => {
5
+ describe('key name patterns', () => {
6
+ test('detects _SECRET suffix', () => {
7
+ expect(looksLikeSecret('API_SECRET', 'value')).toBe(true);
8
+ expect(looksLikeSecret('DB_SECRET', 'value')).toBe(true);
9
+ });
10
+
11
+ test('detects _KEY suffix', () => {
12
+ expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
13
+ expect(looksLikeSecret('STRIPE_KEY', 'value')).toBe(true);
14
+ });
15
+
16
+ test('detects _TOKEN suffix', () => {
17
+ expect(looksLikeSecret('AUTH_TOKEN', 'value')).toBe(true);
18
+ expect(looksLikeSecret('GITHUB_TOKEN', 'value')).toBe(true);
19
+ });
20
+
21
+ test('detects _PASSWORD suffix', () => {
22
+ expect(looksLikeSecret('DB_PASSWORD', 'value')).toBe(true);
23
+ expect(looksLikeSecret('ADMIN_PASSWORD', 'value')).toBe(true);
24
+ });
25
+
26
+ test('detects _PRIVATE suffix', () => {
27
+ expect(looksLikeSecret('SSH_PRIVATE', 'value')).toBe(true);
28
+ });
29
+
30
+ test('detects _CERT and _CERTIFICATE suffixes', () => {
31
+ expect(looksLikeSecret('SSL_CERT', 'value')).toBe(true);
32
+ expect(looksLikeSecret('SSL_CERTIFICATE', 'value')).toBe(true);
33
+ });
34
+
35
+ test('detects SECRET_ prefix', () => {
36
+ expect(looksLikeSecret('SECRET_VALUE', 'value')).toBe(true);
37
+ });
38
+
39
+ test('detects APIKEY and API_KEY patterns', () => {
40
+ expect(looksLikeSecret('APIKEY', 'value')).toBe(true);
41
+ expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
42
+ });
43
+
44
+ test('detects JWT prefix', () => {
45
+ expect(looksLikeSecret('JWT_SECRET', 'value')).toBe(true);
46
+ expect(looksLikeSecret('JWT', 'value')).toBe(true);
47
+ });
48
+
49
+ test('detects PASSWORD in key name', () => {
50
+ expect(looksLikeSecret('DATABASE_PASSWORD', 'value')).toBe(true);
51
+ expect(looksLikeSecret('PASSWORD', 'value')).toBe(true);
52
+ });
53
+
54
+ test('detects CREDENTIAL in key name', () => {
55
+ expect(looksLikeSecret('AWS_CREDENTIALS', 'value')).toBe(true);
56
+ });
57
+
58
+ test('detects AUTH.*KEY pattern', () => {
59
+ expect(looksLikeSecret('AUTH_API_KEY', 'value')).toBe(true);
60
+ expect(looksLikeSecret('AUTHKEY', 'value')).toBe(true);
61
+ });
62
+
63
+ test('is case insensitive for key patterns', () => {
64
+ expect(looksLikeSecret('api_secret', 'value')).toBe(true);
65
+ expect(looksLikeSecret('Api_Key', 'value')).toBe(true);
66
+ expect(looksLikeSecret('AUTH_token', 'value')).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe('value patterns', () => {
71
+ test('detects JWT tokens', () => {
72
+ const jwt =
73
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
74
+ expect(looksLikeSecret('TOKEN', jwt)).toBe(true);
75
+ expect(looksLikeSecret('SOME_VAR', jwt)).toBe(true);
76
+ });
77
+
78
+ test('detects Bearer tokens', () => {
79
+ expect(looksLikeSecret('AUTH', 'Bearer abc123def456ghi789jkl012mno345pqr')).toBe(true);
80
+ });
81
+
82
+ test('detects AWS access keys', () => {
83
+ expect(looksLikeSecret('AWS', 'AKIAIOSFODNN7EXAMPLE')).toBe(true);
84
+ expect(looksLikeSecret('AWS', 'ASIAIOSFODNN7EXAMPLE')).toBe(true);
85
+ });
86
+
87
+ test('detects GitHub tokens', () => {
88
+ expect(looksLikeSecret('GH', 'ghp_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
89
+ expect(looksLikeSecret('GH', 'ghs_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
90
+ });
91
+
92
+ test('detects long alphanumeric strings (API keys)', () => {
93
+ // 32+ characters, mixed alphanumeric
94
+ expect(
95
+ looksLikeSecret('KEY', 'sk_test_51A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6')
96
+ ).toBe(true);
97
+ expect(looksLikeSecret('KEY', 'abc123def456ghi789jkl012mno345pqr678stu901vwx234yz')).toBe(
98
+ true
99
+ );
100
+ });
101
+
102
+ test('does not flag numeric-only long strings', () => {
103
+ expect(looksLikeSecret('ID', '12345678901234567890123456789012')).toBe(false);
104
+ });
105
+
106
+ test('detects PEM certificates', () => {
107
+ expect(looksLikeSecret('CERT', '-----BEGIN CERTIFICATE-----\nMIIC...')).toBe(true);
108
+ expect(looksLikeSecret('CERT', '-----BEGIN PRIVATE KEY-----\nMIIC...')).toBe(true);
109
+ expect(looksLikeSecret('CERT', '-----BEGIN RSA PRIVATE KEY-----\nMIIC...')).toBe(true);
110
+ });
111
+
112
+ test('does not flag short values', () => {
113
+ expect(looksLikeSecret('VAR', 'short')).toBe(false);
114
+ expect(looksLikeSecret('VAR', '1234567')).toBe(false);
115
+ });
116
+
117
+ test('does not flag empty values', () => {
118
+ expect(looksLikeSecret('VAR', '')).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('non-secret patterns', () => {
123
+ test('regular environment variables are not flagged', () => {
124
+ expect(looksLikeSecret('NODE_ENV', 'production')).toBe(false);
125
+ expect(looksLikeSecret('PORT', '3000')).toBe(false);
126
+ expect(looksLikeSecret('HOST', 'localhost')).toBe(false);
127
+ expect(looksLikeSecret('DATABASE_URL', 'postgres://localhost:5432/mydb')).toBe(false);
128
+ });
129
+
130
+ test('configuration values are not flagged', () => {
131
+ expect(looksLikeSecret('LOG_LEVEL', 'debug')).toBe(false);
132
+ expect(looksLikeSecret('CACHE_TTL', '3600')).toBe(false);
133
+ expect(looksLikeSecret('MAX_CONNECTIONS', '100')).toBe(false);
134
+ });
135
+
136
+ test('URLs without secrets are not flagged', () => {
137
+ expect(looksLikeSecret('API_URL', 'https://api.example.com')).toBe(false);
138
+ expect(looksLikeSecret('WEBHOOK_URL', 'https://example.com/webhook')).toBe(false);
139
+ });
140
+
141
+ test('paths are not flagged', () => {
142
+ expect(looksLikeSecret('DATA_PATH', '/var/data/app')).toBe(false);
143
+ expect(looksLikeSecret('CONFIG_FILE', '/etc/app/config.json')).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('edge cases', () => {
148
+ test('handles mixed key and value patterns', () => {
149
+ // Key pattern triggers detection
150
+ expect(looksLikeSecret('API_KEY', 'simple')).toBe(true);
151
+
152
+ // Value pattern triggers detection even without key pattern
153
+ const jwt =
154
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
155
+ expect(looksLikeSecret('CONFIG', jwt)).toBe(true);
156
+ });
157
+
158
+ test('real-world API key formats', () => {
159
+ // Stripe (contains underscore, 32+ chars)
160
+ expect(looksLikeSecret('STRIPE', 'sk_test_51HqL7xAbCdEfGhIjK12345678901234567890')).toBe(
161
+ true
162
+ );
163
+
164
+ // Long API key format (32+ alphanumeric)
165
+ expect(looksLikeSecret('OPENAI', 'sk-proj-1234567890abcdefghijklmnopqrstuvwxyz')).toBe(
166
+ true
167
+ );
168
+
169
+ // Contains dots (periods not in our pattern, but key name helps)
170
+ expect(
171
+ looksLikeSecret(
172
+ 'SENDGRID_API_KEY',
173
+ 'SG.1234567890abcdefghijklmnopqrstuvwxyz.1234567890abcdefghijklmnopqrstuvwxyz'
174
+ )
175
+ ).toBe(true);
176
+ });
177
+
178
+ test('UUIDs are correctly identified as non-secrets', () => {
179
+ // Standard UUID format should not be flagged
180
+ expect(looksLikeSecret('REQUEST_ID', '550e8400-e29b-41d4-a716-446655440000')).toBe(false);
181
+ expect(looksLikeSecret('USER_ID', '123e4567-e89b-12d3-a456-426614174000')).toBe(false);
182
+ });
183
+
184
+ test('hex hashes are flagged (better safe than sorry)', () => {
185
+ // 32+ character hex strings could be secrets or hashes - we flag them
186
+ // Users can confirm they're just hashes if needed
187
+ expect(looksLikeSecret('BUILD_HASH', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6')).toBe(true);
188
+
189
+ // But with context that clearly indicates it's not a secret, the key name won't trigger
190
+ // So short hex strings without secret-like key names won't be flagged
191
+ expect(looksLikeSecret('COMMIT', 'abc123def')).toBe(false);
192
+ });
193
+ });
194
+ });