learn-secrets-sdk 1.2.0 → 1.4.0
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/bin/learn-secrets.js +17 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +113 -0
- package/dist/cli/commands/login.d.ts +7 -0
- package/dist/cli/commands/login.js +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +35 -0
- package/dist/cli/utils/env-parser.d.ts +19 -0
- package/dist/cli/utils/env-parser.js +47 -0
- package/dist/cli/utils/key-detector.d.ts +33 -0
- package/dist/cli/utils/key-detector.js +153 -0
- package/dist/client.d.ts +13 -2
- package/dist/client.js +31 -13
- package/dist/credentials.d.ts +24 -0
- package/dist/credentials.js +86 -0
- package/dist/env-resolver.d.ts +23 -0
- package/dist/env-resolver.js +73 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/management.d.ts +98 -0
- package/dist/management.js +214 -0
- package/dist/types.d.ts +83 -2
- package/package.json +14 -3
- package/secrets.template.json +42 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Entry point for the learn-secrets CLI
|
|
4
|
+
// This file is executed when running: learn-secrets <command>
|
|
5
|
+
|
|
6
|
+
// Check Node version
|
|
7
|
+
const nodeVersion = process.versions.node;
|
|
8
|
+
const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
|
|
9
|
+
|
|
10
|
+
if (majorVersion < 16) {
|
|
11
|
+
console.error('Error: Node.js 16 or higher is required');
|
|
12
|
+
console.error(`You are running Node.js ${nodeVersion}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Run the CLI
|
|
17
|
+
require('../dist/cli/index.js');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
origins: string;
|
|
3
|
+
env?: string;
|
|
4
|
+
project?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
yes?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Init command - onboard a new site by uploading .env secrets
|
|
10
|
+
*/
|
|
11
|
+
export declare function initCommand(options: InitOptions): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { SecretsManagement } from '../../management.js';
|
|
2
|
+
import { parseEnvFile } from '../utils/env-parser.js';
|
|
3
|
+
import { detectApiKeys, summarizeDetectedSecrets } from '../utils/key-detector.js';
|
|
4
|
+
import { hasValidCredentials } from '../../credentials.js';
|
|
5
|
+
/**
|
|
6
|
+
* Init command - onboard a new site by uploading .env secrets
|
|
7
|
+
*/
|
|
8
|
+
export async function initCommand(options) {
|
|
9
|
+
// Validate authentication
|
|
10
|
+
if (!hasValidCredentials()) {
|
|
11
|
+
console.error('Error: Not authenticated. Run "learn-secrets login" first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// Validate required options
|
|
15
|
+
if (!options.origins) {
|
|
16
|
+
console.error('Error: --origins is required');
|
|
17
|
+
console.error('Example: learn-secrets init --origins mysite.com,localhost');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!options.project) {
|
|
21
|
+
console.error('Error: --project is required');
|
|
22
|
+
console.error('Example: learn-secrets init --project my-app-id --origins mysite.com');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Parse origins
|
|
26
|
+
const origins = options.origins.split(',').map(o => o.trim()).filter(Boolean);
|
|
27
|
+
if (origins.length === 0) {
|
|
28
|
+
console.error('Error: No valid origins provided');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log(`\nOnboarding site for project: ${options.project}`);
|
|
32
|
+
console.log(`Origins: ${origins.join(', ')}\n`);
|
|
33
|
+
// Load and detect secrets from .env
|
|
34
|
+
const envFile = options.env || '.env';
|
|
35
|
+
let secrets;
|
|
36
|
+
try {
|
|
37
|
+
console.log(`Reading ${envFile}...`);
|
|
38
|
+
const envVars = parseEnvFile(envFile);
|
|
39
|
+
secrets = detectApiKeys(envVars);
|
|
40
|
+
if (secrets.length === 0) {
|
|
41
|
+
console.log('\nNo API keys detected in .env file.');
|
|
42
|
+
console.log('Make sure your .env contains variables like:');
|
|
43
|
+
console.log(' OPENAI_API_KEY=sk-...');
|
|
44
|
+
console.log(' ANTHROPIC_API_KEY=sk-ant-...');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
console.log('\n' + summarizeDetectedSecrets(secrets));
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error(`Error reading ${envFile}:`, error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Ask for confirmation unless --yes flag
|
|
55
|
+
if (!options.yes) {
|
|
56
|
+
const readline = await import('readline');
|
|
57
|
+
const rl = readline.createInterface({
|
|
58
|
+
input: process.stdin,
|
|
59
|
+
output: process.stdout
|
|
60
|
+
});
|
|
61
|
+
const answer = await new Promise((resolve) => {
|
|
62
|
+
rl.question('Upload these secrets? (y/N): ', resolve);
|
|
63
|
+
});
|
|
64
|
+
rl.close();
|
|
65
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
66
|
+
console.log('Cancelled.');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Upload secrets
|
|
71
|
+
console.log('\nUploading secrets...');
|
|
72
|
+
const mgmt = new SecretsManagement({
|
|
73
|
+
appId: options.project,
|
|
74
|
+
baseUrl: options.baseUrl
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const result = await mgmt.importSecrets({
|
|
78
|
+
version: '1.0',
|
|
79
|
+
origins,
|
|
80
|
+
secrets
|
|
81
|
+
});
|
|
82
|
+
console.log('\nSuccess!');
|
|
83
|
+
console.log(` Created: ${result.results.created} secrets`);
|
|
84
|
+
console.log(` Updated: ${result.results.updated} secrets`);
|
|
85
|
+
if (result.results.failed > 0) {
|
|
86
|
+
console.log(` Failed: ${result.results.failed} secrets`);
|
|
87
|
+
for (const failure of result.results.details.failed) {
|
|
88
|
+
console.log(` - ${failure.name}: ${failure.error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (result.sdkToken) {
|
|
92
|
+
console.log('\nSDK Token created:');
|
|
93
|
+
console.log(` ${result.sdkToken.token}`);
|
|
94
|
+
console.log('\n IMPORTANT: Save this token - it will not be shown again.');
|
|
95
|
+
console.log(' Add it to your static site code:');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(' const sdk = new SecretsSDK();');
|
|
98
|
+
console.log(' // No appId or token needed - uses origin-based auth!');
|
|
99
|
+
}
|
|
100
|
+
console.log('\nYour site is ready!');
|
|
101
|
+
console.log(`Visit: https://ctklearn.carsontkempf.workers.dev to manage secrets`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error('\nUpload failed:', error.message);
|
|
105
|
+
if (error.status === 401) {
|
|
106
|
+
console.error('Your session may have expired. Try running "learn-secrets login" again.');
|
|
107
|
+
}
|
|
108
|
+
if (error.status === 404) {
|
|
109
|
+
console.error(`Project "${options.project}" not found. Check your project ID.`);
|
|
110
|
+
}
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SecretsManagement } from '../../management.js';
|
|
2
|
+
/**
|
|
3
|
+
* Login command - authenticate via browser OAuth flow
|
|
4
|
+
*/
|
|
5
|
+
export async function loginCommand(options = {}) {
|
|
6
|
+
console.log('Authenticating with Learn Secrets...\n');
|
|
7
|
+
// Create management client (appId not needed for login)
|
|
8
|
+
const mgmt = new SecretsManagement({
|
|
9
|
+
appId: 'temp', // Will be set after login
|
|
10
|
+
baseUrl: options.baseUrl
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
const result = await mgmt.login();
|
|
14
|
+
console.log(`\nLogged in as: ${result.user}`);
|
|
15
|
+
console.log('Credentials saved to ~/.learn/credentials.json');
|
|
16
|
+
console.log('\nYou can now run: learn-secrets init --origins yourdomain.com');
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('\nLogin failed:', error.message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { loginCommand } from './commands/login.js';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name('learn-secrets')
|
|
8
|
+
.description('CLI tool for managing API secrets in static sites')
|
|
9
|
+
.version('1.4.0');
|
|
10
|
+
// Login command
|
|
11
|
+
program
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Authenticate with Learn Secrets via browser')
|
|
14
|
+
.option('--base-url <url>', 'Custom base URL for API')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
await loginCommand(options);
|
|
17
|
+
});
|
|
18
|
+
// Init command
|
|
19
|
+
program
|
|
20
|
+
.command('init')
|
|
21
|
+
.description('Initialize secrets for a new site')
|
|
22
|
+
.requiredOption('-o, --origins <domains>', 'Comma-separated list of allowed origins (e.g., mysite.com,localhost)')
|
|
23
|
+
.requiredOption('-p, --project <appid>', 'Project app ID from dashboard')
|
|
24
|
+
.option('-e, --env <file>', 'Path to .env file (default: .env)', '.env')
|
|
25
|
+
.option('--base-url <url>', 'Custom base URL for API')
|
|
26
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
await initCommand(options);
|
|
29
|
+
});
|
|
30
|
+
// Parse arguments
|
|
31
|
+
program.parse(process.argv);
|
|
32
|
+
// Show help if no command provided
|
|
33
|
+
if (!process.argv.slice(2).length) {
|
|
34
|
+
program.outputHelp();
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface EnvVariable {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse a .env file into key-value pairs
|
|
7
|
+
* Supports:
|
|
8
|
+
* - KEY=value
|
|
9
|
+
* - KEY="value"
|
|
10
|
+
* - KEY='value'
|
|
11
|
+
* - # comments
|
|
12
|
+
* - Empty lines
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseEnvFile(filePath: string): EnvVariable[];
|
|
15
|
+
/**
|
|
16
|
+
* Load environment variables from a .env file
|
|
17
|
+
* Returns a Map for easy lookup
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadEnvFile(filePath: string): Map<string, string>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a .env file into key-value pairs
|
|
5
|
+
* Supports:
|
|
6
|
+
* - KEY=value
|
|
7
|
+
* - KEY="value"
|
|
8
|
+
* - KEY='value'
|
|
9
|
+
* - # comments
|
|
10
|
+
* - Empty lines
|
|
11
|
+
*/
|
|
12
|
+
export function parseEnvFile(filePath) {
|
|
13
|
+
const resolvedPath = resolve(filePath);
|
|
14
|
+
if (!existsSync(resolvedPath)) {
|
|
15
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
16
|
+
}
|
|
17
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
18
|
+
const lines = content.split('\n');
|
|
19
|
+
const variables = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
// Skip empty lines and comments
|
|
23
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Match KEY=value, KEY="value", or KEY='value'
|
|
27
|
+
const match = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(['"]?)(.+?)\2$/i);
|
|
28
|
+
if (match) {
|
|
29
|
+
const key = match[1];
|
|
30
|
+
const value = match[3];
|
|
31
|
+
variables.push({ key, value });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return variables;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load environment variables from a .env file
|
|
38
|
+
* Returns a Map for easy lookup
|
|
39
|
+
*/
|
|
40
|
+
export function loadEnvFile(filePath) {
|
|
41
|
+
const variables = parseEnvFile(filePath);
|
|
42
|
+
const map = new Map();
|
|
43
|
+
for (const { key, value } of variables) {
|
|
44
|
+
map.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SecretConfig } from '../../types.js';
|
|
2
|
+
import type { EnvVariable } from './env-parser.js';
|
|
3
|
+
interface ProviderPattern {
|
|
4
|
+
pattern: RegExp;
|
|
5
|
+
provider: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
authHeader?: string;
|
|
8
|
+
authPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Detect if an environment variable is likely an API key
|
|
12
|
+
*/
|
|
13
|
+
export declare function isLikelyApiKey(key: string, value: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Detect provider from environment variable name
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectProvider(key: string): ProviderPattern | null;
|
|
18
|
+
/**
|
|
19
|
+
* Convert environment variable to secret name
|
|
20
|
+
* OPENAI_API_KEY -> openai
|
|
21
|
+
* MY_API_KEY -> my-api
|
|
22
|
+
*/
|
|
23
|
+
export declare function envKeyToSecretName(key: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Detect API keys from environment variables
|
|
26
|
+
* Returns SecretConfig array ready for import
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectApiKeys(envVars: EnvVariable[]): SecretConfig[];
|
|
29
|
+
/**
|
|
30
|
+
* Display detected secrets summary (for user confirmation)
|
|
31
|
+
*/
|
|
32
|
+
export declare function summarizeDetectedSecrets(secrets: SecretConfig[]): string;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common API key patterns and their providers
|
|
3
|
+
*/
|
|
4
|
+
const PROVIDER_PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
pattern: /^OPENAI_API_KEY$/i,
|
|
7
|
+
provider: 'openai',
|
|
8
|
+
baseUrl: 'https://api.openai.com',
|
|
9
|
+
authHeader: 'Authorization',
|
|
10
|
+
authPrefix: 'Bearer '
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /^ANTHROPIC_API_KEY$/i,
|
|
14
|
+
provider: 'anthropic',
|
|
15
|
+
baseUrl: 'https://api.anthropic.com',
|
|
16
|
+
authHeader: 'x-api-key',
|
|
17
|
+
authPrefix: ''
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: /^STRIPE_(SECRET_)?KEY$/i,
|
|
21
|
+
provider: 'stripe',
|
|
22
|
+
baseUrl: 'https://api.stripe.com',
|
|
23
|
+
authHeader: 'Authorization',
|
|
24
|
+
authPrefix: 'Bearer '
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /^GITHUB_TOKEN$/i,
|
|
28
|
+
provider: 'github',
|
|
29
|
+
baseUrl: 'https://api.github.com',
|
|
30
|
+
authHeader: 'Authorization',
|
|
31
|
+
authPrefix: 'Bearer '
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /^GOOGLE_API_KEY$/i,
|
|
35
|
+
provider: 'google',
|
|
36
|
+
baseUrl: 'https://www.googleapis.com',
|
|
37
|
+
authHeader: 'Authorization',
|
|
38
|
+
authPrefix: 'Bearer '
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Detect if an environment variable is likely an API key
|
|
43
|
+
*/
|
|
44
|
+
export function isLikelyApiKey(key, value) {
|
|
45
|
+
// Check common patterns
|
|
46
|
+
const keyPatterns = [
|
|
47
|
+
/_API_KEY$/i,
|
|
48
|
+
/_SECRET$/i,
|
|
49
|
+
/_TOKEN$/i,
|
|
50
|
+
/^API_KEY_/i,
|
|
51
|
+
/^SECRET_/i,
|
|
52
|
+
/^TOKEN_/i
|
|
53
|
+
];
|
|
54
|
+
for (const pattern of keyPatterns) {
|
|
55
|
+
if (pattern.test(key)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Check value patterns (looks like a key/token)
|
|
60
|
+
const valuePatterns = [
|
|
61
|
+
/^sk-[a-zA-Z0-9_-]+$/, // OpenAI, Stripe
|
|
62
|
+
/^sk-ant-[a-zA-Z0-9_-]+$/, // Anthropic
|
|
63
|
+
/^ghp_[a-zA-Z0-9]+$/, // GitHub
|
|
64
|
+
/^ya29\.[a-zA-Z0-9_-]+$/, // Google OAuth
|
|
65
|
+
/^[a-f0-9]{32}$/, // 32-char hex
|
|
66
|
+
/^[a-f0-9]{40}$/, // 40-char hex (like GitHub)
|
|
67
|
+
/^[a-zA-Z0-9_-]{40,}$/ // Long random string
|
|
68
|
+
];
|
|
69
|
+
for (const pattern of valuePatterns) {
|
|
70
|
+
if (pattern.test(value)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect provider from environment variable name
|
|
78
|
+
*/
|
|
79
|
+
export function detectProvider(key) {
|
|
80
|
+
for (const pattern of PROVIDER_PATTERNS) {
|
|
81
|
+
if (pattern.pattern.test(key)) {
|
|
82
|
+
return pattern;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Convert environment variable to secret name
|
|
89
|
+
* OPENAI_API_KEY -> openai
|
|
90
|
+
* MY_API_KEY -> my-api
|
|
91
|
+
*/
|
|
92
|
+
export function envKeyToSecretName(key) {
|
|
93
|
+
return key
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/_api_key$/i, '')
|
|
96
|
+
.replace(/_secret$/i, '')
|
|
97
|
+
.replace(/_token$/i, '')
|
|
98
|
+
.replace(/_key$/i, '')
|
|
99
|
+
.replace(/_/g, '-');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Detect API keys from environment variables
|
|
103
|
+
* Returns SecretConfig array ready for import
|
|
104
|
+
*/
|
|
105
|
+
export function detectApiKeys(envVars) {
|
|
106
|
+
const secrets = [];
|
|
107
|
+
for (const { key, value } of envVars) {
|
|
108
|
+
// Skip if doesn't look like an API key
|
|
109
|
+
if (!isLikelyApiKey(key, value)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Detect provider
|
|
113
|
+
const providerInfo = detectProvider(key);
|
|
114
|
+
const secretName = envKeyToSecretName(key);
|
|
115
|
+
if (providerInfo) {
|
|
116
|
+
// Known provider
|
|
117
|
+
secrets.push({
|
|
118
|
+
name: secretName,
|
|
119
|
+
provider: providerInfo.provider,
|
|
120
|
+
api_key: value,
|
|
121
|
+
base_url: providerInfo.baseUrl,
|
|
122
|
+
auth_header: providerInfo.authHeader,
|
|
123
|
+
auth_prefix: providerInfo.authPrefix
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Custom/unknown provider
|
|
128
|
+
secrets.push({
|
|
129
|
+
name: secretName,
|
|
130
|
+
provider: 'custom',
|
|
131
|
+
api_key: value,
|
|
132
|
+
base_url: '', // User must configure
|
|
133
|
+
auth_header: 'Authorization',
|
|
134
|
+
auth_prefix: 'Bearer '
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return secrets;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Display detected secrets summary (for user confirmation)
|
|
142
|
+
*/
|
|
143
|
+
export function summarizeDetectedSecrets(secrets) {
|
|
144
|
+
if (secrets.length === 0) {
|
|
145
|
+
return 'No API keys detected in .env file';
|
|
146
|
+
}
|
|
147
|
+
const lines = ['Detected API keys:'];
|
|
148
|
+
for (const secret of secrets) {
|
|
149
|
+
const masked = secret.api_key.substring(0, 8) + '...';
|
|
150
|
+
lines.push(` - ${secret.name} (${secret.provider}): ${masked}`);
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SecretsSDKOptions, ProxyRequest, RateLimitInfo } from './types';
|
|
1
|
+
import type { SecretsSDKOptions, ProxyRequest, RateLimitInfo } from './types.js';
|
|
2
2
|
export declare class SecretsSDK {
|
|
3
3
|
private appId;
|
|
4
4
|
private token;
|
|
@@ -6,7 +6,18 @@ export declare class SecretsSDK {
|
|
|
6
6
|
private timeout;
|
|
7
7
|
private retryOn429;
|
|
8
8
|
private rateLimitInfo;
|
|
9
|
-
|
|
9
|
+
private zeroConfigMode;
|
|
10
|
+
/**
|
|
11
|
+
* Create a new SecretsSDK instance
|
|
12
|
+
*
|
|
13
|
+
* Zero-config mode (recommended for static sites):
|
|
14
|
+
* const sdk = new SecretsSDK();
|
|
15
|
+
* // Origin header is used for authentication
|
|
16
|
+
*
|
|
17
|
+
* Token mode (for backward compatibility):
|
|
18
|
+
* const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
|
|
19
|
+
*/
|
|
20
|
+
constructor(options?: SecretsSDKOptions);
|
|
10
21
|
/**
|
|
11
22
|
* Get current rate limit information
|
|
12
23
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
|
|
1
|
+
import { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types.js';
|
|
2
2
|
export class SecretsSDK {
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Create a new SecretsSDK instance
|
|
5
|
+
*
|
|
6
|
+
* Zero-config mode (recommended for static sites):
|
|
7
|
+
* const sdk = new SecretsSDK();
|
|
8
|
+
* // Origin header is used for authentication
|
|
9
|
+
*
|
|
10
|
+
* Token mode (for backward compatibility):
|
|
11
|
+
* const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
4
14
|
this.rateLimitInfo = null;
|
|
5
|
-
this.appId = options.appId;
|
|
6
|
-
this.token = options.token || options.sessionToken ||
|
|
7
|
-
this.baseUrl = options.baseUrl || 'https://
|
|
15
|
+
this.appId = options.appId || null;
|
|
16
|
+
this.token = options.token || options.sessionToken || null;
|
|
17
|
+
this.baseUrl = options.baseUrl || 'https://ctklearn.carsontkempf.workers.dev';
|
|
8
18
|
this.timeout = options.timeout || 30000;
|
|
9
19
|
this.retryOn429 = options.retryOn429 !== undefined ? options.retryOn429 : true;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
// Zero-config mode: no appId or token required
|
|
21
|
+
// Authentication is based on Origin header
|
|
22
|
+
this.zeroConfigMode = !this.appId && !this.token;
|
|
13
23
|
}
|
|
14
24
|
/**
|
|
15
25
|
* Get current rate limit information
|
|
@@ -67,12 +77,20 @@ export class SecretsSDK {
|
|
|
67
77
|
const maxRetries = this.retryOn429 ? 3 : 0;
|
|
68
78
|
while (true) {
|
|
69
79
|
try {
|
|
70
|
-
|
|
80
|
+
// Build URL based on mode
|
|
81
|
+
const proxyUrl = this.zeroConfigMode
|
|
82
|
+
? `${this.baseUrl}/api/sdk/proxy`
|
|
83
|
+
: `${this.baseUrl}/api/sdk/${this.appId}/proxy`;
|
|
84
|
+
// Build headers based on mode
|
|
85
|
+
const requestHeaders = {
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
};
|
|
88
|
+
if (!this.zeroConfigMode && this.token) {
|
|
89
|
+
requestHeaders['Authorization'] = `Bearer ${this.token}`;
|
|
90
|
+
}
|
|
91
|
+
const response = await this.fetchWithTimeout(proxyUrl, {
|
|
71
92
|
method: 'POST',
|
|
72
|
-
headers:
|
|
73
|
-
'Content-Type': 'application/json',
|
|
74
|
-
Authorization: `Bearer ${this.token}`
|
|
75
|
-
},
|
|
93
|
+
headers: requestHeaders,
|
|
76
94
|
body: JSON.stringify({
|
|
77
95
|
keyName,
|
|
78
96
|
endpoint,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CLICredentials } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get the path to the credentials file
|
|
4
|
+
* Defaults to ~/.learn/credentials.json
|
|
5
|
+
*/
|
|
6
|
+
export declare function getCredentialsPath(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Read credentials from file
|
|
9
|
+
* Returns null if file doesn't exist or is invalid
|
|
10
|
+
*/
|
|
11
|
+
export declare function readCredentials(): CLICredentials | null;
|
|
12
|
+
/**
|
|
13
|
+
* Write credentials to file
|
|
14
|
+
* Creates ~/.learn directory if it doesn't exist
|
|
15
|
+
*/
|
|
16
|
+
export declare function writeCredentials(creds: CLICredentials): void;
|
|
17
|
+
/**
|
|
18
|
+
* Delete credentials file
|
|
19
|
+
*/
|
|
20
|
+
export declare function deleteCredentials(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Check if credentials exist and are not expired
|
|
23
|
+
*/
|
|
24
|
+
export declare function hasValidCredentials(): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
/**
|
|
5
|
+
* Get the path to the credentials file
|
|
6
|
+
* Defaults to ~/.learn/credentials.json
|
|
7
|
+
*/
|
|
8
|
+
export function getCredentialsPath() {
|
|
9
|
+
const homeDir = os.homedir();
|
|
10
|
+
const learnDir = path.join(homeDir, '.learn');
|
|
11
|
+
return path.join(learnDir, 'credentials.json');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Ensure the ~/.learn directory exists
|
|
15
|
+
*/
|
|
16
|
+
function ensureLearnDir() {
|
|
17
|
+
const homeDir = os.homedir();
|
|
18
|
+
const learnDir = path.join(homeDir, '.learn');
|
|
19
|
+
if (!fs.existsSync(learnDir)) {
|
|
20
|
+
fs.mkdirSync(learnDir, { recursive: true, mode: 0o700 }); // Owner-only permissions
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read credentials from file
|
|
25
|
+
* Returns null if file doesn't exist or is invalid
|
|
26
|
+
*/
|
|
27
|
+
export function readCredentials() {
|
|
28
|
+
try {
|
|
29
|
+
const credPath = getCredentialsPath();
|
|
30
|
+
if (!fs.existsSync(credPath)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const data = fs.readFileSync(credPath, 'utf-8');
|
|
34
|
+
const creds = JSON.parse(data);
|
|
35
|
+
// Validate required fields
|
|
36
|
+
if (!creds.access_token ||
|
|
37
|
+
!creds.refresh_token ||
|
|
38
|
+
!creds.expires_at ||
|
|
39
|
+
!creds.user_id) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return creds;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Invalid JSON or read error
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write credentials to file
|
|
51
|
+
* Creates ~/.learn directory if it doesn't exist
|
|
52
|
+
*/
|
|
53
|
+
export function writeCredentials(creds) {
|
|
54
|
+
ensureLearnDir();
|
|
55
|
+
const credPath = getCredentialsPath();
|
|
56
|
+
const data = JSON.stringify(creds, null, 2);
|
|
57
|
+
// Write with owner-only permissions
|
|
58
|
+
fs.writeFileSync(credPath, data, { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Delete credentials file
|
|
62
|
+
*/
|
|
63
|
+
export function deleteCredentials() {
|
|
64
|
+
try {
|
|
65
|
+
const credPath = getCredentialsPath();
|
|
66
|
+
if (fs.existsSync(credPath)) {
|
|
67
|
+
fs.unlinkSync(credPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// Ignore errors (file might not exist)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if credentials exist and are not expired
|
|
76
|
+
*/
|
|
77
|
+
export function hasValidCredentials() {
|
|
78
|
+
const creds = readCredentials();
|
|
79
|
+
if (!creds) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// Check if expired
|
|
83
|
+
const expiresAt = new Date(creds.expires_at);
|
|
84
|
+
const now = new Date();
|
|
85
|
+
return expiresAt > now;
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SecretConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve environment variable references in a string
|
|
4
|
+
* Supports ${VAR} and $VAR syntax
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveEnvString(value: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve environment variables in a single secret config
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveEnvInSecret(secret: SecretConfig): SecretConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve environment variables in an array of secrets
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveEnvInSecrets(secrets: SecretConfig[]): SecretConfig[];
|
|
15
|
+
/**
|
|
16
|
+
* Load and parse a secrets config file
|
|
17
|
+
* Resolves environment variables in the config
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadSecretsConfig(configPath: string): {
|
|
20
|
+
version: string;
|
|
21
|
+
project: string;
|
|
22
|
+
secrets: SecretConfig[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve environment variable references in a string
|
|
3
|
+
* Supports ${VAR} and $VAR syntax
|
|
4
|
+
*/
|
|
5
|
+
export function resolveEnvString(value) {
|
|
6
|
+
// Replace ${VAR} and $VAR references
|
|
7
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g, (match, var1, var2) => {
|
|
8
|
+
const varName = var1 || var2;
|
|
9
|
+
const envValue = process.env[varName];
|
|
10
|
+
if (envValue === undefined) {
|
|
11
|
+
throw new Error(`Environment variable ${varName} is not defined`);
|
|
12
|
+
}
|
|
13
|
+
return envValue;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve environment variables in a single secret config
|
|
18
|
+
*/
|
|
19
|
+
export function resolveEnvInSecret(secret) {
|
|
20
|
+
const resolved = {
|
|
21
|
+
name: resolveEnvString(secret.name),
|
|
22
|
+
provider: resolveEnvString(secret.provider),
|
|
23
|
+
api_key: resolveEnvString(secret.api_key)
|
|
24
|
+
};
|
|
25
|
+
if (secret.base_url) {
|
|
26
|
+
resolved.base_url = resolveEnvString(secret.base_url);
|
|
27
|
+
}
|
|
28
|
+
if (secret.auth_header) {
|
|
29
|
+
resolved.auth_header = resolveEnvString(secret.auth_header);
|
|
30
|
+
}
|
|
31
|
+
if (secret.auth_prefix) {
|
|
32
|
+
resolved.auth_prefix = resolveEnvString(secret.auth_prefix);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve environment variables in an array of secrets
|
|
38
|
+
*/
|
|
39
|
+
export function resolveEnvInSecrets(secrets) {
|
|
40
|
+
return secrets.map(resolveEnvInSecret);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load and parse a secrets config file
|
|
44
|
+
* Resolves environment variables in the config
|
|
45
|
+
*/
|
|
46
|
+
export function loadSecretsConfig(configPath) {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
// Read config file
|
|
50
|
+
const fullPath = path.resolve(configPath);
|
|
51
|
+
if (!fs.existsSync(fullPath)) {
|
|
52
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
53
|
+
}
|
|
54
|
+
const configData = fs.readFileSync(fullPath, 'utf-8');
|
|
55
|
+
const config = JSON.parse(configData);
|
|
56
|
+
// Validate config
|
|
57
|
+
if (!config.version) {
|
|
58
|
+
throw new Error('Config file missing "version" field');
|
|
59
|
+
}
|
|
60
|
+
if (!config.project) {
|
|
61
|
+
throw new Error('Config file missing "project" field');
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(config.secrets)) {
|
|
64
|
+
throw new Error('Config file missing "secrets" array');
|
|
65
|
+
}
|
|
66
|
+
// Resolve environment variables
|
|
67
|
+
const resolvedSecrets = resolveEnvInSecrets(config.secrets);
|
|
68
|
+
return {
|
|
69
|
+
version: config.version,
|
|
70
|
+
project: config.project,
|
|
71
|
+
secrets: resolvedSecrets
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { SecretsSDK } from './client';
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
1
|
+
export { SecretsSDK } from './client.js';
|
|
2
|
+
export { SecretsManagement } from './management.js';
|
|
3
|
+
export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver.js';
|
|
4
|
+
export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types.js';
|
|
5
|
+
export type { SecretsSDKOptions, ProxyRequest, ProxyResponse, RateLimitInfo, SecretsManagementOptions, SecretConfig, MaskedSecret, SyncOptions, SyncResult, CLICredentials } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { SecretsSDK } from './client';
|
|
2
|
-
export {
|
|
1
|
+
export { SecretsSDK } from './client.js';
|
|
2
|
+
export { SecretsManagement } from './management.js';
|
|
3
|
+
export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver.js';
|
|
4
|
+
export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type SecretsManagementOptions, type SecretConfig, type MaskedSecret, type SyncOptions, type SyncResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* SecretsManagement class for CLI authentication and secrets management
|
|
4
|
+
*/
|
|
5
|
+
export declare class SecretsManagement {
|
|
6
|
+
private appId;
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private timeout;
|
|
9
|
+
constructor(options: SecretsManagementOptions);
|
|
10
|
+
/**
|
|
11
|
+
* Open a URL in the default browser
|
|
12
|
+
*/
|
|
13
|
+
private openBrowser;
|
|
14
|
+
/**
|
|
15
|
+
* Sleep for specified milliseconds
|
|
16
|
+
*/
|
|
17
|
+
private sleep;
|
|
18
|
+
/**
|
|
19
|
+
* Browser OAuth login flow
|
|
20
|
+
* Opens browser for user to authorize, then stores credentials locally
|
|
21
|
+
*/
|
|
22
|
+
login(): Promise<{
|
|
23
|
+
success: boolean;
|
|
24
|
+
user: string;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if user is authenticated
|
|
28
|
+
*/
|
|
29
|
+
isAuthenticated(): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Logout - clear stored credentials
|
|
32
|
+
*/
|
|
33
|
+
logout(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get access token from stored credentials
|
|
36
|
+
* Throws error if not authenticated
|
|
37
|
+
*/
|
|
38
|
+
private getAccessToken;
|
|
39
|
+
/**
|
|
40
|
+
* Make authenticated request
|
|
41
|
+
*/
|
|
42
|
+
private request;
|
|
43
|
+
/**
|
|
44
|
+
* List all secrets (with masked API keys)
|
|
45
|
+
*/
|
|
46
|
+
list(): Promise<MaskedSecret[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Sync secrets from config
|
|
49
|
+
*/
|
|
50
|
+
sync(secrets: SecretConfig[], options?: SyncOptions): Promise<SyncResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Import secrets from a config object (bulk import)
|
|
53
|
+
* Also creates SDK token if origins are provided
|
|
54
|
+
*
|
|
55
|
+
* @param config - The secrets configuration
|
|
56
|
+
* @returns Import results including created/updated counts and optional SDK token
|
|
57
|
+
*/
|
|
58
|
+
importSecrets(config: {
|
|
59
|
+
version?: string;
|
|
60
|
+
origins?: string[];
|
|
61
|
+
secrets: SecretConfig[];
|
|
62
|
+
}): Promise<{
|
|
63
|
+
success: boolean;
|
|
64
|
+
results: {
|
|
65
|
+
created: number;
|
|
66
|
+
updated: number;
|
|
67
|
+
failed: number;
|
|
68
|
+
details: {
|
|
69
|
+
created: string[];
|
|
70
|
+
updated: string[];
|
|
71
|
+
failed: {
|
|
72
|
+
name: string;
|
|
73
|
+
error: string;
|
|
74
|
+
}[];
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
sdkToken?: {
|
|
78
|
+
token: string;
|
|
79
|
+
message: string;
|
|
80
|
+
};
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Import secrets from a JSON file path
|
|
84
|
+
* Resolves environment variables in the config
|
|
85
|
+
*/
|
|
86
|
+
importFromFile(configPath: string): Promise<{
|
|
87
|
+
success: boolean;
|
|
88
|
+
results: {
|
|
89
|
+
created: number;
|
|
90
|
+
updated: number;
|
|
91
|
+
failed: number;
|
|
92
|
+
};
|
|
93
|
+
sdkToken?: {
|
|
94
|
+
token: string;
|
|
95
|
+
message: string;
|
|
96
|
+
};
|
|
97
|
+
}>;
|
|
98
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { SecretsSDKError } from './types.js';
|
|
2
|
+
import { readCredentials, writeCredentials, deleteCredentials, hasValidCredentials } from './credentials.js';
|
|
3
|
+
/**
|
|
4
|
+
* SecretsManagement class for CLI authentication and secrets management
|
|
5
|
+
*/
|
|
6
|
+
export class SecretsManagement {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.appId = options.appId;
|
|
9
|
+
this.baseUrl = options.baseUrl || 'https://ctklearn.carsontkempf.workers.dev';
|
|
10
|
+
this.timeout = options.timeout || 30000;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Open a URL in the default browser
|
|
14
|
+
*/
|
|
15
|
+
async openBrowser(url) {
|
|
16
|
+
const { exec } = await import('child_process');
|
|
17
|
+
const { promisify } = await import('util');
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
try {
|
|
21
|
+
if (platform === 'darwin') {
|
|
22
|
+
await execAsync(`open "${url}"`);
|
|
23
|
+
}
|
|
24
|
+
else if (platform === 'win32') {
|
|
25
|
+
await execAsync(`start "${url}"`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Linux
|
|
29
|
+
await execAsync(`xdg-open "${url}"`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error('Failed to open browser:', err);
|
|
34
|
+
throw new SecretsSDKError('Failed to open browser', 500);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sleep for specified milliseconds
|
|
39
|
+
*/
|
|
40
|
+
sleep(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Browser OAuth login flow
|
|
45
|
+
* Opens browser for user to authorize, then stores credentials locally
|
|
46
|
+
*/
|
|
47
|
+
async login() {
|
|
48
|
+
try {
|
|
49
|
+
// Step 1: Request device code
|
|
50
|
+
const deviceResponse = await fetch(`${this.baseUrl}/api/auth/cli/device`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json'
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (!deviceResponse.ok) {
|
|
57
|
+
throw new SecretsSDKError('Failed to generate device code', deviceResponse.status);
|
|
58
|
+
}
|
|
59
|
+
const deviceData = await deviceResponse.json();
|
|
60
|
+
// Step 2: Display user code and verification URI
|
|
61
|
+
console.log('\nTo authorize this application, visit:');
|
|
62
|
+
console.log(` ${deviceData.verification_uri}`);
|
|
63
|
+
console.log('\nAnd enter the code:');
|
|
64
|
+
console.log(` ${deviceData.user_code}`);
|
|
65
|
+
console.log('\nOpening browser...\n');
|
|
66
|
+
// Step 3: Open browser
|
|
67
|
+
await this.openBrowser(deviceData.verification_uri_complete);
|
|
68
|
+
// Step 4: Poll for token
|
|
69
|
+
const interval = deviceData.interval * 1000; // Convert to ms
|
|
70
|
+
const maxAttempts = Math.ceil(deviceData.expires_in / deviceData.interval);
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
while (attempts < maxAttempts) {
|
|
73
|
+
await this.sleep(interval);
|
|
74
|
+
attempts++;
|
|
75
|
+
const tokenResponse = await fetch(`${this.baseUrl}/api/auth/cli/token`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json'
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
device_code: deviceData.device_code
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
if (tokenResponse.status === 202) {
|
|
85
|
+
// Still pending
|
|
86
|
+
process.stdout.write('.');
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!tokenResponse.ok) {
|
|
90
|
+
const errorData = await tokenResponse.json().catch(() => ({}));
|
|
91
|
+
throw new SecretsSDKError(errorData.message || 'Failed to exchange device code', tokenResponse.status);
|
|
92
|
+
}
|
|
93
|
+
// Success!
|
|
94
|
+
const tokenData = await tokenResponse.json();
|
|
95
|
+
// Step 5: Store credentials
|
|
96
|
+
const expiresAt = new Date(Date.now() + tokenData.expires_in * 1000);
|
|
97
|
+
writeCredentials({
|
|
98
|
+
access_token: tokenData.access_token,
|
|
99
|
+
refresh_token: tokenData.refresh_token,
|
|
100
|
+
expires_at: expiresAt.toISOString(),
|
|
101
|
+
user_id: tokenData.user_id
|
|
102
|
+
});
|
|
103
|
+
console.log('\n\nAuthentication successful!');
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
user: tokenData.user_id
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Timeout
|
|
110
|
+
throw new SecretsSDKError('Authorization timeout', 408);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof SecretsSDKError) {
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
throw new SecretsSDKError(err.message || 'Login failed', err.status || 500);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if user is authenticated
|
|
121
|
+
*/
|
|
122
|
+
async isAuthenticated() {
|
|
123
|
+
return hasValidCredentials();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Logout - clear stored credentials
|
|
127
|
+
*/
|
|
128
|
+
logout() {
|
|
129
|
+
deleteCredentials();
|
|
130
|
+
console.log('Logged out successfully');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get access token from stored credentials
|
|
134
|
+
* Throws error if not authenticated
|
|
135
|
+
*/
|
|
136
|
+
getAccessToken() {
|
|
137
|
+
const creds = readCredentials();
|
|
138
|
+
if (!creds) {
|
|
139
|
+
throw new SecretsSDKError('Not authenticated. Run login() first.', 401);
|
|
140
|
+
}
|
|
141
|
+
// Check if expired
|
|
142
|
+
const expiresAt = new Date(creds.expires_at);
|
|
143
|
+
const now = new Date();
|
|
144
|
+
if (expiresAt <= now) {
|
|
145
|
+
throw new SecretsSDKError('Token expired. Run login() again.', 401);
|
|
146
|
+
}
|
|
147
|
+
return creds.access_token;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Make authenticated request
|
|
151
|
+
*/
|
|
152
|
+
async request(method, path, body) {
|
|
153
|
+
const token = this.getAccessToken();
|
|
154
|
+
const options = {
|
|
155
|
+
method,
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
Authorization: `Bearer ${token}`
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
if (body) {
|
|
162
|
+
options.body = JSON.stringify(body);
|
|
163
|
+
}
|
|
164
|
+
const response = await fetch(`${this.baseUrl}${path}`, options);
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const errorData = await response.json().catch(() => ({}));
|
|
167
|
+
throw new SecretsSDKError(errorData.message || `Request failed: ${response.statusText}`, response.status, errorData);
|
|
168
|
+
}
|
|
169
|
+
return response.json();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List all secrets (with masked API keys)
|
|
173
|
+
*/
|
|
174
|
+
async list() {
|
|
175
|
+
const response = await this.request('GET', `/api/projects/${this.appId}/secrets`);
|
|
176
|
+
return response.secrets;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Sync secrets from config
|
|
180
|
+
*/
|
|
181
|
+
async sync(secrets, options) {
|
|
182
|
+
const response = await this.request('POST', `/api/projects/${this.appId}/secrets/sync`, {
|
|
183
|
+
secrets,
|
|
184
|
+
options: {
|
|
185
|
+
deleteMissing: options?.deleteMissing ?? false,
|
|
186
|
+
dryRun: options?.dryRun ?? false
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return response;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Import secrets from a config object (bulk import)
|
|
193
|
+
* Also creates SDK token if origins are provided
|
|
194
|
+
*
|
|
195
|
+
* @param config - The secrets configuration
|
|
196
|
+
* @returns Import results including created/updated counts and optional SDK token
|
|
197
|
+
*/
|
|
198
|
+
async importSecrets(config) {
|
|
199
|
+
return this.request('POST', `/api/projects/${this.appId}/import-secrets`, {
|
|
200
|
+
version: config.version || '1.0',
|
|
201
|
+
origins: config.origins,
|
|
202
|
+
secrets: config.secrets
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Import secrets from a JSON file path
|
|
207
|
+
* Resolves environment variables in the config
|
|
208
|
+
*/
|
|
209
|
+
async importFromFile(configPath) {
|
|
210
|
+
const { loadSecretsConfig } = await import('./env-resolver');
|
|
211
|
+
const config = loadSecretsConfig(configPath);
|
|
212
|
+
return this.importSecrets(config);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
export interface SecretsSDKOptions {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* App ID from dashboard (optional in zero-config mode)
|
|
4
|
+
* If omitted, SDK uses origin-based authentication
|
|
5
|
+
*/
|
|
6
|
+
appId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* SDK token (optional in zero-config mode)
|
|
9
|
+
* If omitted, SDK uses origin-based authentication
|
|
10
|
+
*/
|
|
11
|
+
token?: string;
|
|
4
12
|
sessionToken?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Base URL of the proxy server
|
|
15
|
+
* Default: https://ctklearn.carsontkempf.workers.dev
|
|
16
|
+
*/
|
|
5
17
|
baseUrl?: string;
|
|
6
18
|
timeout?: number;
|
|
7
19
|
retryOn429?: boolean;
|
|
@@ -40,3 +52,72 @@ export declare class RateLimitError extends SecretsSDKError {
|
|
|
40
52
|
export declare class InvalidTokenError extends SecretsSDKError {
|
|
41
53
|
constructor(message?: string);
|
|
42
54
|
}
|
|
55
|
+
export interface SecretsManagementOptions {
|
|
56
|
+
appId: string;
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
timeout?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface SecretConfig {
|
|
61
|
+
name: string;
|
|
62
|
+
provider: string;
|
|
63
|
+
api_key: string;
|
|
64
|
+
base_url?: string;
|
|
65
|
+
auth_header?: string;
|
|
66
|
+
auth_prefix?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MaskedSecret {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
provider: string;
|
|
72
|
+
api_key: string;
|
|
73
|
+
base_url?: string;
|
|
74
|
+
auth_header?: string;
|
|
75
|
+
auth_prefix?: string;
|
|
76
|
+
created?: string;
|
|
77
|
+
updated?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface SyncOptions {
|
|
80
|
+
deleteMissing?: boolean;
|
|
81
|
+
dryRun?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface SyncDiffItem {
|
|
84
|
+
name: string;
|
|
85
|
+
provider?: string;
|
|
86
|
+
changes?: string[];
|
|
87
|
+
}
|
|
88
|
+
export interface SyncResult {
|
|
89
|
+
success: boolean;
|
|
90
|
+
diff: {
|
|
91
|
+
created: SyncDiffItem[];
|
|
92
|
+
updated: SyncDiffItem[];
|
|
93
|
+
deleted: SyncDiffItem[];
|
|
94
|
+
unchanged: SyncDiffItem[];
|
|
95
|
+
};
|
|
96
|
+
summary: {
|
|
97
|
+
created: number;
|
|
98
|
+
updated: number;
|
|
99
|
+
deleted: number;
|
|
100
|
+
unchanged: number;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export interface DeviceCodeResponse {
|
|
104
|
+
device_code: string;
|
|
105
|
+
user_code: string;
|
|
106
|
+
verification_uri: string;
|
|
107
|
+
verification_uri_complete: string;
|
|
108
|
+
expires_in: number;
|
|
109
|
+
interval: number;
|
|
110
|
+
}
|
|
111
|
+
export interface TokenResponse {
|
|
112
|
+
access_token: string;
|
|
113
|
+
refresh_token: string;
|
|
114
|
+
token_type: string;
|
|
115
|
+
expires_in: number;
|
|
116
|
+
user_id: string;
|
|
117
|
+
}
|
|
118
|
+
export interface CLICredentials {
|
|
119
|
+
access_token: string;
|
|
120
|
+
refresh_token: string;
|
|
121
|
+
expires_at: string;
|
|
122
|
+
user_id: string;
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "learn-secrets-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Secure API proxy SDK for static sites with domain-scoped tokens",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"learn-secrets": "./bin/learn-secrets.js"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"secrets.template.json"
|
|
10
15
|
],
|
|
11
16
|
"scripts": {
|
|
12
17
|
"build": "tsc && tsc --project tsconfig.esm.json",
|
|
@@ -17,11 +22,17 @@
|
|
|
17
22
|
"proxy",
|
|
18
23
|
"secrets",
|
|
19
24
|
"static-sites",
|
|
20
|
-
"security"
|
|
25
|
+
"security",
|
|
26
|
+
"cli",
|
|
27
|
+
"secrets-management"
|
|
21
28
|
],
|
|
22
29
|
"author": "",
|
|
23
30
|
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^12.0.0"
|
|
33
|
+
},
|
|
24
34
|
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
25
36
|
"typescript": "^5.0.0"
|
|
26
37
|
}
|
|
27
38
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"project": "your-app-id",
|
|
4
|
+
"origins": [
|
|
5
|
+
"localhost",
|
|
6
|
+
"yourdomain.com"
|
|
7
|
+
],
|
|
8
|
+
"secrets": [
|
|
9
|
+
{
|
|
10
|
+
"name": "test-echo",
|
|
11
|
+
"provider": "custom",
|
|
12
|
+
"api_key": "test",
|
|
13
|
+
"base_url": "https://httpbin.org",
|
|
14
|
+
"auth_header": "Authorization",
|
|
15
|
+
"auth_prefix": "Bearer "
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "openai",
|
|
19
|
+
"provider": "openai",
|
|
20
|
+
"api_key": "${OPENAI_API_KEY}",
|
|
21
|
+
"base_url": "https://api.openai.com",
|
|
22
|
+
"auth_header": "Authorization",
|
|
23
|
+
"auth_prefix": "Bearer "
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "anthropic",
|
|
27
|
+
"provider": "anthropic",
|
|
28
|
+
"api_key": "${ANTHROPIC_API_KEY}",
|
|
29
|
+
"base_url": "https://api.anthropic.com",
|
|
30
|
+
"auth_header": "x-api-key",
|
|
31
|
+
"auth_prefix": ""
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "stripe",
|
|
35
|
+
"provider": "stripe",
|
|
36
|
+
"api_key": "${STRIPE_SECRET_KEY}",
|
|
37
|
+
"base_url": "https://api.stripe.com",
|
|
38
|
+
"auth_header": "Authorization",
|
|
39
|
+
"auth_prefix": "Bearer "
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|