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 +206 -0
- package/dist/client.d.ts +33 -3
- package/dist/client.js +127 -32
- 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 +4 -2
- package/dist/index.js +3 -1
- package/dist/management.d.ts +51 -0
- package/dist/management.js +191 -0
- package/dist/types.d.ts +103 -2
- package/dist/types.js +21 -0
- package/package.json +2 -2
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
|
|
4
|
+
private token;
|
|
5
5
|
private baseUrl;
|
|
6
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
3
|
-
export
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
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.
|
|
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",
|