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/README.md CHANGED
@@ -1,51 +1,102 @@
1
1
  # learn-secrets-sdk
2
2
 
3
- secure api proxy sdk for static sites. call external apis (openai, anthropic, stripe, etc.) without exposing api keys.
3
+ secure api proxy for static sites. call external apis (openai, anthropic, stripe, etc.) without exposing api keys.
4
4
 
5
- ## quick start
5
+ **zero-config:** no tokens or credentials in your code. authentication via origin header.
6
6
 
7
- ### 1. setup in dashboard
7
+ ## quick start (cli)
8
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
9
+ ### 1. install cli
14
10
 
15
- ### 2. install
11
+ ```bash
12
+ npm install -g learn-secrets-sdk
13
+ ```
14
+
15
+ ### 2. authenticate
16
+
17
+ ```bash
18
+ learn-secrets login
19
+ ```
20
+
21
+ ### 3. initialize project
22
+
23
+ ```bash
24
+ learn-secrets init --project YOUR_PROJECT_ID --origins yourdomain.com,localhost --env .env
25
+ ```
26
+
27
+ auto-detects api keys from `.env` and uploads them. configures origins for your site.
28
+
29
+ **get project id:** visit [ctklearn.carsontkempf.workers.dev](https://ctklearn.carsontkempf.workers.dev) → copy from sidebar
30
+
31
+ ### 4. install sdk in project
16
32
 
17
33
  ```bash
18
34
  npm install learn-secrets-sdk
19
35
  ```
20
36
 
21
- ### 3. use in your app
37
+ ### 5. use in code
22
38
 
23
39
  ```typescript
24
40
  import { SecretsSDK } from 'learn-secrets-sdk';
25
41
 
26
- const sdk = new SecretsSDK({
27
- appId: 'your-app-id',
28
- token: 'sk_live_...'
29
- });
42
+ // zero-config - no credentials needed
43
+ const sdk = new SecretsSDK();
30
44
 
31
- // call openai
32
- const response = await sdk.post('my-openai-key', '/v1/chat/completions', {
45
+ // call openai securely
46
+ const response = await sdk.post('openai', '/v1/chat/completions', {
33
47
  model: 'gpt-4',
34
48
  messages: [{ role: 'user', content: 'hello' }]
35
49
  });
36
50
  ```
37
51
 
52
+ **how it works:**
53
+ * browser sends `Origin` header automatically
54
+ * proxy validates origin matches configured domains
55
+ * api key injected server-side
56
+ * no tokens or credentials in client code
57
+
38
58
  ## features
39
59
 
40
- * **domain-scoped tokens** - tokens only work from configured origins
41
- * **zero api key exposure** - keys stay server-side, never sent to client
60
+ * **zero-config** - no tokens, appid, or credentials in code
61
+ * **origin-based auth** - automatic browser security via origin header
62
+ * **api keys secured** - stored server-side, never exposed to clients
63
+ * **auto-detection** - cli detects openai, anthropic, stripe, github, google
42
64
  * **works anywhere** - cloudflare pages, netlify, vercel, any static host
43
65
  * **rate limiting** - 100 req/min per domain built-in
44
- * **typescript support** - full type definitions included
66
+ * **typescript** - full type definitions included
67
+
68
+ ## cli commands
69
+
70
+ ### login
71
+ ```bash
72
+ learn-secrets login
73
+ ```
74
+ authenticate via browser oauth. credentials saved to `~/.learn/credentials.json`.
75
+
76
+ ### init
77
+ ```bash
78
+ learn-secrets init \
79
+ --project abc123xyz \ # your project id
80
+ --origins example.com,localhost \ # allowed domains
81
+ --env .env # path to .env file
82
+ ```
83
+
84
+ reads .env, detects api keys, uploads to project, configures origins.
85
+
86
+ **auto-detects:**
87
+ * `OPENAI_API_KEY` → openai provider
88
+ * `ANTHROPIC_API_KEY` → anthropic provider
89
+ * `STRIPE_SECRET_KEY` → stripe provider
90
+ * `GITHUB_TOKEN` → github provider
91
+ * `*_API_KEY` → custom provider
92
+
93
+ full cli docs: [cli reference](https://ctklearn.carsontkempf.workers.dev/docs/sdk/cli)
45
94
 
46
95
  ## api methods
47
96
 
48
97
  ```typescript
98
+ const sdk = new SecretsSDK();
99
+
49
100
  // http methods
50
101
  await sdk.get(keyName, endpoint, headers?)
51
102
  await sdk.post(keyName, endpoint, body, headers?)
@@ -63,14 +114,19 @@ await sdk.call(keyName, endpoint, {
63
114
 
64
115
  ## constructor options
65
116
 
117
+ **zero-config (recommended):**
66
118
  ```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
- })
119
+ const sdk = new SecretsSDK();
120
+ ```
121
+ uses origin header for authentication. no credentials needed.
122
+
123
+ **advanced options:**
124
+ ```typescript
125
+ const sdk = new SecretsSDK({
126
+ baseUrl?: string, // custom proxy url (default: production)
127
+ timeout?: number, // request timeout ms (default: 30000)
128
+ retryOn429?: boolean // auto-retry on rate limit (default: true)
129
+ });
74
130
  ```
75
131
 
76
132
  ## error handling
@@ -78,17 +134,21 @@ new SecretsSDK({
78
134
  ```typescript
79
135
  import { SecretsSDK, SecretsSDKError, OriginMismatchError, RateLimitError } from 'learn-secrets-sdk';
80
136
 
137
+ const sdk = new SecretsSDK();
138
+
81
139
  try {
82
- const response = await sdk.post('key', '/endpoint', data);
140
+ const response = await sdk.post('openai', '/v1/chat/completions', data);
83
141
  } catch (error) {
84
142
  if (error instanceof OriginMismatchError) {
85
- // domain not in allowed origins
143
+ // domain not configured in allowed origins
144
+ console.error('add domain to origins in dashboard');
86
145
  } else if (error instanceof RateLimitError) {
87
146
  // exceeded 100 req/min
88
- console.log('retry after:', error.retryAfter);
147
+ console.log('retry after:', error.retryAfter, 'seconds');
89
148
  } else if (error instanceof SecretsSDKError) {
90
149
  // other api errors
91
150
  console.log('status:', error.status);
151
+ console.log('message:', error.message);
92
152
  }
93
153
  }
94
154
  ```
@@ -96,34 +156,46 @@ try {
96
156
  ## usage tracking
97
157
 
98
158
  ```typescript
99
- const sdk = new SecretsSDK({ appId, token });
159
+ const sdk = new SecretsSDK();
100
160
 
101
- await sdk.post('key', '/endpoint', data);
161
+ await sdk.post('openai', '/v1/chat/completions', data);
102
162
 
103
163
  // check rate limit status
104
164
  const usage = sdk.getUsage();
105
165
  console.log('remaining:', usage.remaining); // requests left this minute
166
+ console.log('limit:', usage.limit); // 100
106
167
  console.log('resets at:', usage.reset); // unix timestamp
107
168
  ```
108
169
 
109
170
  ## examples
110
171
 
111
- ### vanilla html
172
+ ### vanilla javascript
112
173
 
113
174
  ```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>
175
+ <!DOCTYPE html>
176
+ <html>
177
+ <body>
178
+ <button onclick="callAPI()">call ai</button>
179
+ <div id="response"></div>
180
+
181
+ <script type="module">
182
+ import { SecretsSDK } from './node_modules/learn-secrets-sdk/dist/index.js';
183
+
184
+ // zero-config
185
+ const sdk = new SecretsSDK();
186
+
187
+ window.callAPI = async function() {
188
+ const result = await sdk.post('openai', '/v1/chat/completions', {
189
+ model: 'gpt-4',
190
+ messages: [{ role: 'user', content: 'hello' }]
191
+ });
192
+
193
+ document.getElementById('response').textContent =
194
+ result.choices[0].message.content;
195
+ };
196
+ </script>
197
+ </body>
198
+ </html>
127
199
  ```
128
200
 
129
201
  ### react
@@ -133,17 +205,18 @@ import { useMemo } from 'react';
133
205
  import { SecretsSDK } from 'learn-secrets-sdk';
134
206
 
135
207
  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
- }), []);
208
+ // zero-config sdk
209
+ const sdk = useMemo(() => new SecretsSDK(), []);
140
210
 
141
211
  const handleClick = async () => {
142
- const result = await sdk.post('my-openai-key', '/v1/chat/completions', {
212
+ const result = await sdk.post('openai', '/v1/chat/completions', {
143
213
  model: 'gpt-4',
144
214
  messages: [{ role: 'user', content: 'hello' }]
145
215
  });
216
+ console.log(result.choices[0].message.content);
146
217
  };
218
+
219
+ return <button onClick={handleClick}>call ai</button>;
147
220
  }
148
221
  ```
149
222
 
@@ -153,15 +226,12 @@ function App() {
153
226
  import { ref } from 'vue';
154
227
  import { SecretsSDK } from 'learn-secrets-sdk';
155
228
 
156
- const sdk = new SecretsSDK({
157
- appId: import.meta.env.VITE_APP_ID,
158
- token: import.meta.env.VITE_SDK_TOKEN
159
- });
160
-
229
+ // zero-config sdk
230
+ const sdk = new SecretsSDK();
161
231
  const response = ref('');
162
232
 
163
233
  async function callAPI() {
164
- const result = await sdk.post('my-openai-key', '/v1/chat/completions', {
234
+ const result = await sdk.post('openai', '/v1/chat/completions', {
165
235
  model: 'gpt-4',
166
236
  messages: [{ role: 'user', content: 'hello' }]
167
237
  });
@@ -169,37 +239,87 @@ async function callAPI() {
169
239
  }
170
240
  ```
171
241
 
242
+ ### next.js
243
+
244
+ ```typescript
245
+ 'use client';
246
+
247
+ import { useCallback, useMemo } from 'react';
248
+ import { SecretsSDK } from 'learn-secrets-sdk';
249
+
250
+ export default function ChatPage() {
251
+ // zero-config sdk
252
+ const sdk = useMemo(() => new SecretsSDK(), []);
253
+
254
+ const chat = useCallback(async (message: string) => {
255
+ const result = await sdk.post('openai', '/v1/chat/completions', {
256
+ model: 'gpt-4',
257
+ messages: [{ role: 'user', content: message }]
258
+ });
259
+ return result.choices[0].message.content;
260
+ }, [sdk]);
261
+
262
+ // ... rest of component
263
+ }
264
+ ```
265
+
172
266
  ## supported providers
173
267
 
174
- * openai
175
- * anthropic
176
- * stripe
177
- * any http-based api
268
+ auto-configured base urls when using cli:
269
+ * openai (`OPENAI_API_KEY`)
270
+ * anthropic (`ANTHROPIC_API_KEY`)
271
+ * stripe (`STRIPE_SECRET_KEY`)
272
+ * github (`GITHUB_TOKEN`)
273
+ * google (`GOOGLE_API_KEY`)
274
+
275
+ custom providers supported - configure base url in dashboard.
178
276
 
179
277
  ## how it works
180
278
 
181
279
  ```
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.)
280
+ 1. cli uploads api keys to proxy server
281
+ 2. cli configures allowed origins (yourdomain.com, localhost, etc.)
282
+ 3. your static site imports sdk: new SecretsSDK()
283
+ 4. browser automatically sends Origin header
284
+ 5. proxy validates origin matches configured list
285
+ 6. proxy injects api key server-side
286
+ 7. external api called with key
287
+ 8. response returned to browser
189
288
  ```
190
289
 
290
+ **security:**
291
+ * api keys never sent to browser
292
+ * origin header enforced by browser (cannot be spoofed)
293
+ * stolen code useless from unauthorized domains
294
+
191
295
  ## security
192
296
 
193
297
  * **api keys never exposed** - stored and injected server-side only
194
- * **domain-scoped tokens** - browser origin header enforced, can't be spoofed
298
+ * **origin-based auth** - browser origin header enforced, can't be faked
299
+ * **no credentials in code** - zero-config means no tokens to leak
195
300
  * **rate limiting** - 100 req/min per domain prevents abuse
196
- * **instant revocation** - revoke tokens anytime in dashboard
301
+ * **instant updates** - change origins in dashboard anytime
197
302
 
198
303
  ## documentation
199
304
 
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)
305
+ * [quick start guide](https://ctklearn.carsontkempf.workers.dev/docs/sdk/quick-start)
306
+ * [cli reference](https://ctklearn.carsontkempf.workers.dev/docs/sdk/cli)
307
+ * [full api reference](https://ctklearn.carsontkempf.workers.dev/docs/sdk/secrets-proxy)
308
+ * [dashboard](https://ctklearn.carsontkempf.workers.dev)
309
+
310
+ ## troubleshooting
311
+
312
+ **403 origin not allowed:**
313
+ * run `learn-secrets init` with correct `--origins`
314
+ * or update origins in dashboard: settings → sdk tokens
315
+
316
+ **404 api key not found:**
317
+ * verify key name matches .env variable (e.g., `OPENAI_API_KEY` → `openai`)
318
+ * re-run `learn-secrets init` to upload keys
319
+
320
+ **429 rate limit:**
321
+ * 100 requests per minute per domain
322
+ * implement throttling or caching in your app
203
323
 
204
324
  ## license
205
325
 
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Entry point for the learn-secrets CLI
4
+ // This file is executed when running: learn-secrets <command>
5
+
6
+ // Check Node version
7
+ const nodeVersion = process.versions.node;
8
+ const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
9
+
10
+ if (majorVersion < 16) {
11
+ console.error('Error: Node.js 16 or higher is required');
12
+ console.error(`You are running Node.js ${nodeVersion}`);
13
+ process.exit(1);
14
+ }
15
+
16
+ // Run the CLI (ESM module)
17
+ import('../dist/cli/index.mjs').catch((err) => {
18
+ console.error('Failed to load CLI:', err.message);
19
+ process.exit(1);
20
+ });
@@ -0,0 +1 @@
1
+ var f=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});function i(e){return e.replace(/\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g,(r,t,o)=>{let s=t||o,n=process.env[s];if(n===void 0)throw new Error(`Environment variable ${s} is not defined`);return n})}function c(e){let r={name:i(e.name),provider:i(e.provider),api_key:i(e.api_key)};return e.base_url&&(r.base_url=i(e.base_url)),e.auth_header&&(r.auth_header=i(e.auth_header)),e.auth_prefix&&(r.auth_prefix=i(e.auth_prefix)),r}function p(e){return e.map(c)}function g(e){let r=f("fs"),o=f("path").resolve(e);if(!r.existsSync(o))throw new Error(`Config file not found: ${o}`);let s=r.readFileSync(o,"utf-8"),n=JSON.parse(s);if(!n.version)throw new Error('Config file missing "version" field');if(!n.project)throw new Error('Config file missing "project" field');if(!Array.isArray(n.secrets))throw new Error('Config file missing "secrets" array');let a=p(n.secrets);return{version:n.version,project:n.project,secrets:a}}export{i as a,c as b,p as c,g as d};
@@ -0,0 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ export {
9
+ __require
10
+ };
@@ -0,0 +1,66 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-Y6FXYEAI.mjs";
4
+
5
+ // src/env-resolver.ts
6
+ function resolveEnvString(value) {
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 === void 0) {
11
+ throw new Error(`Environment variable ${varName} is not defined`);
12
+ }
13
+ return envValue;
14
+ });
15
+ }
16
+ function resolveEnvInSecret(secret) {
17
+ const resolved = {
18
+ name: resolveEnvString(secret.name),
19
+ provider: resolveEnvString(secret.provider),
20
+ api_key: resolveEnvString(secret.api_key)
21
+ };
22
+ if (secret.base_url) {
23
+ resolved.base_url = resolveEnvString(secret.base_url);
24
+ }
25
+ if (secret.auth_header) {
26
+ resolved.auth_header = resolveEnvString(secret.auth_header);
27
+ }
28
+ if (secret.auth_prefix) {
29
+ resolved.auth_prefix = resolveEnvString(secret.auth_prefix);
30
+ }
31
+ return resolved;
32
+ }
33
+ function resolveEnvInSecrets(secrets) {
34
+ return secrets.map(resolveEnvInSecret);
35
+ }
36
+ function loadSecretsConfig(configPath) {
37
+ const fs = __require("fs");
38
+ const path = __require("path");
39
+ const fullPath = path.resolve(configPath);
40
+ if (!fs.existsSync(fullPath)) {
41
+ throw new Error(`Config file not found: ${fullPath}`);
42
+ }
43
+ const configData = fs.readFileSync(fullPath, "utf-8");
44
+ const config = JSON.parse(configData);
45
+ if (!config.version) {
46
+ throw new Error('Config file missing "version" field');
47
+ }
48
+ if (!config.project) {
49
+ throw new Error('Config file missing "project" field');
50
+ }
51
+ if (!Array.isArray(config.secrets)) {
52
+ throw new Error('Config file missing "secrets" array');
53
+ }
54
+ const resolvedSecrets = resolveEnvInSecrets(config.secrets);
55
+ return {
56
+ version: config.version,
57
+ project: config.project,
58
+ secrets: resolvedSecrets
59
+ };
60
+ }
61
+ export {
62
+ loadSecretsConfig,
63
+ resolveEnvInSecret,
64
+ resolveEnvInSecrets,
65
+ resolveEnvString
66
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node