learn-secrets-sdk 1.0.0 → 1.2.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,28 @@
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
+ private timeout;
7
+ private retryOn429;
8
+ private rateLimitInfo;
6
9
  constructor(options: SecretsSDKOptions);
10
+ /**
11
+ * Get current rate limit information
12
+ */
13
+ getUsage(): RateLimitInfo | null;
14
+ /**
15
+ * Parse rate limit headers from response
16
+ */
17
+ private parseRateLimitHeaders;
18
+ /**
19
+ * Sleep utility for retry backoff
20
+ */
21
+ private sleep;
22
+ /**
23
+ * Make fetch request with timeout
24
+ */
25
+ private fetchWithTimeout;
7
26
  /**
8
27
  * Call an external API securely through the proxy
9
28
  * @param keyName - Name of the API key to use
package/dist/client.js CHANGED
@@ -1,9 +1,59 @@
1
- import { SecretsSDKError } from './types';
1
+ import { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
2
2
  export class SecretsSDK {
3
3
  constructor(options) {
4
+ this.rateLimitInfo = null;
4
5
  this.appId = options.appId;
5
- this.sessionToken = options.sessionToken;
6
+ this.token = options.token || options.sessionToken || '';
6
7
  this.baseUrl = options.baseUrl || 'https://learn.pages.dev';
8
+ this.timeout = options.timeout || 30000;
9
+ this.retryOn429 = options.retryOn429 !== undefined ? options.retryOn429 : true;
10
+ if (!this.token) {
11
+ throw new Error('token is required');
12
+ }
13
+ }
14
+ /**
15
+ * Get current rate limit information
16
+ */
17
+ getUsage() {
18
+ return this.rateLimitInfo;
19
+ }
20
+ /**
21
+ * Parse rate limit headers from response
22
+ */
23
+ parseRateLimitHeaders(headers) {
24
+ const limit = headers.get('X-RateLimit-Limit');
25
+ const remaining = headers.get('X-RateLimit-Remaining');
26
+ const reset = headers.get('X-RateLimit-Reset');
27
+ if (limit && remaining && reset) {
28
+ this.rateLimitInfo = {
29
+ limit: parseInt(limit),
30
+ remaining: parseInt(remaining),
31
+ reset: parseInt(reset)
32
+ };
33
+ }
34
+ }
35
+ /**
36
+ * Sleep utility for retry backoff
37
+ */
38
+ sleep(ms) {
39
+ return new Promise((resolve) => setTimeout(resolve, ms));
40
+ }
41
+ /**
42
+ * Make fetch request with timeout
43
+ */
44
+ async fetchWithTimeout(url, options, timeout) {
45
+ const controller = new AbortController();
46
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
47
+ try {
48
+ const response = await fetch(url, {
49
+ ...options,
50
+ signal: controller.signal
51
+ });
52
+ return response;
53
+ }
54
+ finally {
55
+ clearTimeout(timeoutId);
56
+ }
7
57
  }
8
58
  /**
9
59
  * Call an external API securely through the proxy
@@ -13,35 +63,62 @@ export class SecretsSDK {
13
63
  * @returns Promise with the API response
14
64
  */
15
65
  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);
66
+ let retries = 0;
67
+ const maxRetries = this.retryOn429 ? 3 : 0;
68
+ while (true) {
69
+ try {
70
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/api/sdk/${this.appId}/proxy`, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ Authorization: `Bearer ${this.token}`
75
+ },
76
+ body: JSON.stringify({
77
+ keyName,
78
+ endpoint,
79
+ method: options.method || 'GET',
80
+ body: options.body,
81
+ headers: options.headers
82
+ })
83
+ }, this.timeout);
84
+ this.parseRateLimitHeaders(response.headers);
85
+ const result = await response.json();
86
+ if (!response.ok) {
87
+ const errorMessage = result.data?.message ||
88
+ result?.message ||
89
+ `Request failed with status ${response.status}`;
90
+ if (response.status === 403 && errorMessage.includes('Origin')) {
91
+ throw new OriginMismatchError(errorMessage);
92
+ }
93
+ if (response.status === 401) {
94
+ throw new InvalidTokenError(errorMessage);
95
+ }
96
+ if (response.status === 429) {
97
+ const retryAfter = this.rateLimitInfo
98
+ ? this.rateLimitInfo.reset - Math.floor(Date.now() / 1000)
99
+ : 60;
100
+ const error = new RateLimitError(errorMessage, retryAfter, this.rateLimitInfo?.remaining || 0, this.rateLimitInfo?.limit || 100);
101
+ if (this.retryOn429 && retries < maxRetries) {
102
+ retries++;
103
+ const backoffMs = Math.min(1000 * Math.pow(2, retries - 1), 8000);
104
+ await this.sleep(backoffMs);
105
+ continue;
106
+ }
107
+ throw error;
108
+ }
109
+ throw new SecretsSDKError(errorMessage, response.status, result);
110
+ }
111
+ return result.data;
37
112
  }
38
- return result.data;
39
- }
40
- catch (err) {
41
- if (err instanceof SecretsSDKError) {
42
- throw err;
113
+ catch (err) {
114
+ if (err instanceof SecretsSDKError) {
115
+ throw err;
116
+ }
117
+ if (err instanceof Error && err.name === 'AbortError') {
118
+ throw new SecretsSDKError('Request timeout', 408);
119
+ }
120
+ throw new SecretsSDKError(err instanceof Error ? err.message : 'Unknown error occurred', 500);
43
121
  }
44
- throw new SecretsSDKError(err instanceof Error ? err.message : 'Unknown error occurred', 500);
45
122
  }
46
123
  }
47
124
  /**
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { SecretsSDK } from './client';
2
- export { SecretsSDKError } from './types';
3
- export type { SecretsSDKOptions, ProxyRequest, ProxyResponse } from './types';
2
+ export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
3
+ export type { SecretsSDKOptions, ProxyRequest, ProxyResponse, RateLimitInfo } from './types';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { SecretsSDK } from './client';
2
- export { SecretsSDKError } from './types';
2
+ export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
package/dist/types.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export interface SecretsSDKOptions {
2
2
  appId: string;
3
- sessionToken: string;
3
+ token: string;
4
+ sessionToken?: string;
4
5
  baseUrl?: string;
6
+ timeout?: number;
7
+ retryOn429?: boolean;
5
8
  }
6
9
  export interface ProxyRequest {
7
10
  keyName: string;
@@ -15,8 +18,25 @@ export interface ProxyResponse<T = any> {
15
18
  status: number;
16
19
  data: T;
17
20
  }
21
+ export interface RateLimitInfo {
22
+ limit: number;
23
+ remaining: number;
24
+ reset: number;
25
+ }
18
26
  export declare class SecretsSDKError extends Error {
19
27
  status: number;
20
28
  response?: any | undefined;
21
29
  constructor(message: string, status: number, response?: any | undefined);
22
30
  }
31
+ export declare class OriginMismatchError extends SecretsSDKError {
32
+ constructor(message?: string);
33
+ }
34
+ export declare class RateLimitError extends SecretsSDKError {
35
+ retryAfter: number;
36
+ remaining: number;
37
+ limit: number;
38
+ constructor(message?: string, retryAfter?: number, remaining?: number, limit?: number);
39
+ }
40
+ export declare class InvalidTokenError extends SecretsSDKError {
41
+ constructor(message?: string);
42
+ }
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.2.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",