learn-secrets-sdk 1.0.0 → 1.3.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/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # learn-secrets-sdk
2
+
3
+ secure api proxy sdk for static sites. call external apis (openai, anthropic, stripe, etc.) without exposing api keys.
4
+
5
+ ## quick start
6
+
7
+ ### 1. setup in dashboard
8
+
9
+ * login at [learn.pages.dev](https://learn.pages.dev)
10
+ * add api key in secrets → api_keys
11
+ * create sdk token in settings → sdk tokens
12
+ * configure allowed origins (e.g., `example.com`, `*.example.com`)
13
+ * copy app id from sidebar
14
+
15
+ ### 2. install
16
+
17
+ ```bash
18
+ npm install learn-secrets-sdk
19
+ ```
20
+
21
+ ### 3. use in your app
22
+
23
+ ```typescript
24
+ import { SecretsSDK } from 'learn-secrets-sdk';
25
+
26
+ const sdk = new SecretsSDK({
27
+ appId: 'your-app-id',
28
+ token: 'sk_live_...'
29
+ });
30
+
31
+ // call openai
32
+ const response = await sdk.post('my-openai-key', '/v1/chat/completions', {
33
+ model: 'gpt-4',
34
+ messages: [{ role: 'user', content: 'hello' }]
35
+ });
36
+ ```
37
+
38
+ ## features
39
+
40
+ * **domain-scoped tokens** - tokens only work from configured origins
41
+ * **zero api key exposure** - keys stay server-side, never sent to client
42
+ * **works anywhere** - cloudflare pages, netlify, vercel, any static host
43
+ * **rate limiting** - 100 req/min per domain built-in
44
+ * **typescript support** - full type definitions included
45
+
46
+ ## api methods
47
+
48
+ ```typescript
49
+ // http methods
50
+ await sdk.get(keyName, endpoint, headers?)
51
+ await sdk.post(keyName, endpoint, body, headers?)
52
+ await sdk.put(keyName, endpoint, body, headers?)
53
+ await sdk.delete(keyName, endpoint, headers?)
54
+ await sdk.patch(keyName, endpoint, body, headers?)
55
+
56
+ // custom request
57
+ await sdk.call(keyName, endpoint, {
58
+ method: 'POST',
59
+ body: { },
60
+ headers: { }
61
+ })
62
+ ```
63
+
64
+ ## constructor options
65
+
66
+ ```typescript
67
+ new SecretsSDK({
68
+ appId: string, // required - from dashboard
69
+ token: string, // required - from settings → sdk tokens
70
+ baseUrl?: string, // optional - defaults to https://learn.pages.dev
71
+ timeout?: number, // optional - request timeout in ms (default: 30000)
72
+ retryOn429?: boolean // optional - auto-retry on rate limit (default: true)
73
+ })
74
+ ```
75
+
76
+ ## error handling
77
+
78
+ ```typescript
79
+ import { SecretsSDK, SecretsSDKError, OriginMismatchError, RateLimitError } from 'learn-secrets-sdk';
80
+
81
+ try {
82
+ const response = await sdk.post('key', '/endpoint', data);
83
+ } catch (error) {
84
+ if (error instanceof OriginMismatchError) {
85
+ // domain not in allowed origins
86
+ } else if (error instanceof RateLimitError) {
87
+ // exceeded 100 req/min
88
+ console.log('retry after:', error.retryAfter);
89
+ } else if (error instanceof SecretsSDKError) {
90
+ // other api errors
91
+ console.log('status:', error.status);
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## usage tracking
97
+
98
+ ```typescript
99
+ const sdk = new SecretsSDK({ appId, token });
100
+
101
+ await sdk.post('key', '/endpoint', data);
102
+
103
+ // check rate limit status
104
+ const usage = sdk.getUsage();
105
+ console.log('remaining:', usage.remaining); // requests left this minute
106
+ console.log('resets at:', usage.reset); // unix timestamp
107
+ ```
108
+
109
+ ## examples
110
+
111
+ ### vanilla html
112
+
113
+ ```html
114
+ <script type="module">
115
+ import { SecretsSDK } from 'https://unpkg.com/learn-secrets-sdk';
116
+
117
+ const sdk = new SecretsSDK({
118
+ appId: 'abc123',
119
+ token: 'sk_live_...'
120
+ });
121
+
122
+ const result = await sdk.post('my-openai-key', '/v1/chat/completions', {
123
+ model: 'gpt-4',
124
+ messages: [{ role: 'user', content: 'hello' }]
125
+ });
126
+ </script>
127
+ ```
128
+
129
+ ### react
130
+
131
+ ```typescript
132
+ import { useMemo } from 'react';
133
+ import { SecretsSDK } from 'learn-secrets-sdk';
134
+
135
+ function App() {
136
+ const sdk = useMemo(() => new SecretsSDK({
137
+ appId: import.meta.env.VITE_APP_ID,
138
+ token: import.meta.env.VITE_SDK_TOKEN
139
+ }), []);
140
+
141
+ const handleClick = async () => {
142
+ const result = await sdk.post('my-openai-key', '/v1/chat/completions', {
143
+ model: 'gpt-4',
144
+ messages: [{ role: 'user', content: 'hello' }]
145
+ });
146
+ };
147
+ }
148
+ ```
149
+
150
+ ### vue
151
+
152
+ ```typescript
153
+ import { ref } from 'vue';
154
+ import { SecretsSDK } from 'learn-secrets-sdk';
155
+
156
+ const sdk = new SecretsSDK({
157
+ appId: import.meta.env.VITE_APP_ID,
158
+ token: import.meta.env.VITE_SDK_TOKEN
159
+ });
160
+
161
+ const response = ref('');
162
+
163
+ async function callAPI() {
164
+ const result = await sdk.post('my-openai-key', '/v1/chat/completions', {
165
+ model: 'gpt-4',
166
+ messages: [{ role: 'user', content: 'hello' }]
167
+ });
168
+ response.value = result.choices[0].message.content;
169
+ }
170
+ ```
171
+
172
+ ## supported providers
173
+
174
+ * openai
175
+ * anthropic
176
+ * stripe
177
+ * any http-based api
178
+
179
+ ## how it works
180
+
181
+ ```
182
+ your static site → sdk token (domain-restricted)
183
+
184
+ proxy server (validates origin)
185
+
186
+ injects api key (server-side)
187
+
188
+ external api (openai, etc.)
189
+ ```
190
+
191
+ ## security
192
+
193
+ * **api keys never exposed** - stored and injected server-side only
194
+ * **domain-scoped tokens** - browser origin header enforced, can't be spoofed
195
+ * **rate limiting** - 100 req/min per domain prevents abuse
196
+ * **instant revocation** - revoke tokens anytime in dashboard
197
+
198
+ ## documentation
199
+
200
+ * [quick start guide](https://learn.pages.dev/docs/sdk/quick-start)
201
+ * [full api reference](https://learn.pages.dev/docs/sdk/secrets-proxy)
202
+ * [dashboard](https://learn.pages.dev)
203
+
204
+ ## license
205
+
206
+ mit
package/dist/client.d.ts CHANGED
@@ -1,9 +1,39 @@
1
- import type { SecretsSDKOptions, ProxyRequest } from './types';
1
+ import type { SecretsSDKOptions, ProxyRequest, RateLimitInfo } from './types';
2
2
  export declare class SecretsSDK {
3
3
  private appId;
4
- private sessionToken;
4
+ private token;
5
5
  private baseUrl;
6
- constructor(options: SecretsSDKOptions);
6
+ private timeout;
7
+ private retryOn429;
8
+ private rateLimitInfo;
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);
21
+ /**
22
+ * Get current rate limit information
23
+ */
24
+ getUsage(): RateLimitInfo | null;
25
+ /**
26
+ * Parse rate limit headers from response
27
+ */
28
+ private parseRateLimitHeaders;
29
+ /**
30
+ * Sleep utility for retry backoff
31
+ */
32
+ private sleep;
33
+ /**
34
+ * Make fetch request with timeout
35
+ */
36
+ private fetchWithTimeout;
7
37
  /**
8
38
  * Call an external API securely through the proxy
9
39
  * @param keyName - Name of the API key to use
package/dist/client.js CHANGED
@@ -1,9 +1,69 @@
1
- import { SecretsSDKError } from './types';
1
+ import { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
2
2
  export class SecretsSDK {
3
- constructor(options) {
4
- this.appId = options.appId;
5
- this.sessionToken = options.sessionToken;
6
- this.baseUrl = options.baseUrl || 'https://learn.pages.dev';
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
+ }
7
67
  }
8
68
  /**
9
69
  * Call an external API securely through the proxy
@@ -13,35 +73,70 @@ export class SecretsSDK {
13
73
  * @returns Promise with the API response
14
74
  */
15
75
  async call(keyName, endpoint, options = {}) {
16
- try {
17
- const response = await fetch(`${this.baseUrl}/api/sdk/${this.appId}/proxy`, {
18
- method: 'POST',
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- Authorization: `Bearer ${this.sessionToken}`
22
- },
23
- body: JSON.stringify({
24
- keyName,
25
- endpoint,
26
- method: options.method || 'GET',
27
- body: options.body,
28
- headers: options.headers
29
- })
30
- });
31
- const result = await response.json();
32
- if (!response.ok) {
33
- const errorMessage = result.data?.message ||
34
- result?.message ||
35
- `Request failed with status ${response.status}`;
36
- throw new SecretsSDKError(errorMessage, response.status, result);
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;
37
130
  }
38
- return result.data;
39
- }
40
- catch (err) {
41
- if (err instanceof SecretsSDKError) {
42
- throw err;
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);
43
139
  }
44
- throw new SecretsSDKError(err instanceof Error ? err.message : 'Unknown error occurred', 500);
45
140
  }
46
141
  }
47
142
  /**
@@ -0,0 +1,24 @@
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;
@@ -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';
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
1
  export { SecretsSDK } from './client';
2
- export { SecretsSDKError } from './types';
3
- export type { SecretsSDKOptions, ProxyRequest, ProxyResponse } from './types';
2
+ export { SecretsManagement } from './management';
3
+ export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver';
4
+ export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
5
+ export type { SecretsSDKOptions, ProxyRequest, ProxyResponse, RateLimitInfo, SecretsManagementOptions, SecretConfig, MaskedSecret, SyncOptions, SyncResult, CLICredentials } from './types';
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { SecretsSDK } from './client';
2
- export { SecretsSDKError } from './types';
2
+ export { SecretsManagement } from './management';
3
+ export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver';
4
+ export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,191 @@
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
+ }
package/dist/types.d.ts CHANGED
@@ -1,7 +1,22 @@
1
1
  export interface SecretsSDKOptions {
2
- appId: string;
3
- sessionToken: string;
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;
12
+ sessionToken?: string;
13
+ /**
14
+ * Base URL of the proxy server
15
+ * Default: https://ctklearn.carsontkempf.workers.dev
16
+ */
4
17
  baseUrl?: string;
18
+ timeout?: number;
19
+ retryOn429?: boolean;
5
20
  }
6
21
  export interface ProxyRequest {
7
22
  keyName: string;
@@ -15,8 +30,94 @@ export interface ProxyResponse<T = any> {
15
30
  status: number;
16
31
  data: T;
17
32
  }
33
+ export interface RateLimitInfo {
34
+ limit: number;
35
+ remaining: number;
36
+ reset: number;
37
+ }
18
38
  export declare class SecretsSDKError extends Error {
19
39
  status: number;
20
40
  response?: any | undefined;
21
41
  constructor(message: string, status: number, response?: any | undefined);
22
42
  }
43
+ export declare class OriginMismatchError extends SecretsSDKError {
44
+ constructor(message?: string);
45
+ }
46
+ export declare class RateLimitError extends SecretsSDKError {
47
+ retryAfter: number;
48
+ remaining: number;
49
+ limit: number;
50
+ constructor(message?: string, retryAfter?: number, remaining?: number, limit?: number);
51
+ }
52
+ export declare class InvalidTokenError extends SecretsSDKError {
53
+ constructor(message?: string);
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/dist/types.js CHANGED
@@ -6,3 +6,24 @@ export class SecretsSDKError extends Error {
6
6
  this.name = 'SecretsSDKError';
7
7
  }
8
8
  }
9
+ export class OriginMismatchError extends SecretsSDKError {
10
+ constructor(message = 'Origin not allowed for this token') {
11
+ super(message, 403);
12
+ this.name = 'OriginMismatchError';
13
+ }
14
+ }
15
+ export class RateLimitError extends SecretsSDKError {
16
+ constructor(message = 'Rate limit exceeded', retryAfter = 60, remaining = 0, limit = 100) {
17
+ super(message, 429);
18
+ this.name = 'RateLimitError';
19
+ this.retryAfter = retryAfter;
20
+ this.remaining = remaining;
21
+ this.limit = limit;
22
+ }
23
+ }
24
+ export class InvalidTokenError extends SecretsSDKError {
25
+ constructor(message = 'Invalid or expired token') {
26
+ super(message, 401);
27
+ this.name = 'InvalidTokenError';
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "learn-secrets-sdk",
3
- "version": "1.0.0",
4
- "description": "Secure API proxy SDK for static sites",
3
+ "version": "1.3.0",
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",