learn-secrets-sdk 1.3.0 → 1.5.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/dist/client.js DELETED
@@ -1,172 +0,0 @@
1
- import { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
2
- export class SecretsSDK {
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 = {}) {
14
- this.rateLimitInfo = null;
15
- this.appId = options.appId || null;
16
- this.token = options.token || options.sessionToken || null;
17
- this.baseUrl = options.baseUrl || 'https://ctklearn.carsontkempf.workers.dev';
18
- this.timeout = options.timeout || 30000;
19
- this.retryOn429 = options.retryOn429 !== undefined ? options.retryOn429 : true;
20
- // Zero-config mode: no appId or token required
21
- // Authentication is based on Origin header
22
- this.zeroConfigMode = !this.appId && !this.token;
23
- }
24
- /**
25
- * Get current rate limit information
26
- */
27
- getUsage() {
28
- return this.rateLimitInfo;
29
- }
30
- /**
31
- * Parse rate limit headers from response
32
- */
33
- parseRateLimitHeaders(headers) {
34
- const limit = headers.get('X-RateLimit-Limit');
35
- const remaining = headers.get('X-RateLimit-Remaining');
36
- const reset = headers.get('X-RateLimit-Reset');
37
- if (limit && remaining && reset) {
38
- this.rateLimitInfo = {
39
- limit: parseInt(limit),
40
- remaining: parseInt(remaining),
41
- reset: parseInt(reset)
42
- };
43
- }
44
- }
45
- /**
46
- * Sleep utility for retry backoff
47
- */
48
- sleep(ms) {
49
- return new Promise((resolve) => setTimeout(resolve, ms));
50
- }
51
- /**
52
- * Make fetch request with timeout
53
- */
54
- async fetchWithTimeout(url, options, timeout) {
55
- const controller = new AbortController();
56
- const timeoutId = setTimeout(() => controller.abort(), timeout);
57
- try {
58
- const response = await fetch(url, {
59
- ...options,
60
- signal: controller.signal
61
- });
62
- return response;
63
- }
64
- finally {
65
- clearTimeout(timeoutId);
66
- }
67
- }
68
- /**
69
- * Call an external API securely through the proxy
70
- * @param keyName - Name of the API key to use
71
- * @param endpoint - API endpoint path (e.g., '/v1/chat/completions')
72
- * @param options - Request options (method, body, headers)
73
- * @returns Promise with the API response
74
- */
75
- async call(keyName, endpoint, options = {}) {
76
- let retries = 0;
77
- const maxRetries = this.retryOn429 ? 3 : 0;
78
- while (true) {
79
- try {
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, {
92
- method: 'POST',
93
- headers: requestHeaders,
94
- body: JSON.stringify({
95
- keyName,
96
- endpoint,
97
- method: options.method || 'GET',
98
- body: options.body,
99
- headers: options.headers
100
- })
101
- }, this.timeout);
102
- this.parseRateLimitHeaders(response.headers);
103
- const result = await response.json();
104
- if (!response.ok) {
105
- const errorMessage = result.data?.message ||
106
- result?.message ||
107
- `Request failed with status ${response.status}`;
108
- if (response.status === 403 && errorMessage.includes('Origin')) {
109
- throw new OriginMismatchError(errorMessage);
110
- }
111
- if (response.status === 401) {
112
- throw new InvalidTokenError(errorMessage);
113
- }
114
- if (response.status === 429) {
115
- const retryAfter = this.rateLimitInfo
116
- ? this.rateLimitInfo.reset - Math.floor(Date.now() / 1000)
117
- : 60;
118
- const error = new RateLimitError(errorMessage, retryAfter, this.rateLimitInfo?.remaining || 0, this.rateLimitInfo?.limit || 100);
119
- if (this.retryOn429 && retries < maxRetries) {
120
- retries++;
121
- const backoffMs = Math.min(1000 * Math.pow(2, retries - 1), 8000);
122
- await this.sleep(backoffMs);
123
- continue;
124
- }
125
- throw error;
126
- }
127
- throw new SecretsSDKError(errorMessage, response.status, result);
128
- }
129
- return result.data;
130
- }
131
- catch (err) {
132
- if (err instanceof SecretsSDKError) {
133
- throw err;
134
- }
135
- if (err instanceof Error && err.name === 'AbortError') {
136
- throw new SecretsSDKError('Request timeout', 408);
137
- }
138
- throw new SecretsSDKError(err instanceof Error ? err.message : 'Unknown error occurred', 500);
139
- }
140
- }
141
- }
142
- /**
143
- * Make a GET request
144
- */
145
- async get(keyName, endpoint, headers) {
146
- return this.call(keyName, endpoint, { method: 'GET', headers });
147
- }
148
- /**
149
- * Make a POST request
150
- */
151
- async post(keyName, endpoint, body, headers) {
152
- return this.call(keyName, endpoint, { method: 'POST', body, headers });
153
- }
154
- /**
155
- * Make a PUT request
156
- */
157
- async put(keyName, endpoint, body, headers) {
158
- return this.call(keyName, endpoint, { method: 'PUT', body, headers });
159
- }
160
- /**
161
- * Make a DELETE request
162
- */
163
- async delete(keyName, endpoint, headers) {
164
- return this.call(keyName, endpoint, { method: 'DELETE', headers });
165
- }
166
- /**
167
- * Make a PATCH request
168
- */
169
- async patch(keyName, endpoint, body, headers) {
170
- return this.call(keyName, endpoint, { method: 'PATCH', body, headers });
171
- }
172
- }
@@ -1,24 +0,0 @@
1
- import type { CLICredentials } from './types';
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;
@@ -1,86 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
1
- import type { SecretConfig } from './types';
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
- };
@@ -1,73 +0,0 @@
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.js DELETED
@@ -1,4 +0,0 @@
1
- export { SecretsSDK } from './client';
2
- export { SecretsManagement } from './management';
3
- export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver';
4
- export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
@@ -1,51 +0,0 @@
1
- import { type SecretsManagementOptions, type SecretConfig, type MaskedSecret, type SyncOptions, type SyncResult } from './types';
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
- }
@@ -1,191 +0,0 @@
1
- import { SecretsSDKError } from './types';
2
- import { readCredentials, writeCredentials, deleteCredentials, hasValidCredentials } from './credentials';
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
- }