learn-secrets-sdk 1.11.1 → 1.11.3

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
@@ -294,11 +294,104 @@ custom providers supported - configure base url in dashboard.
294
294
 
295
295
  ## security
296
296
 
297
- * **api keys never exposed** - stored and injected server-side only
298
- * **origin-based auth** - browser origin header enforced, can't be faked
299
- * **no credentials in code** - zero-config means no tokens to leak
297
+ ### security model
298
+
299
+ the sdk uses origin-based authentication specifically designed for static sites. here's how it protects your api keys:
300
+
301
+ **zero credentials in code:**
302
+ * no api keys, tokens, or appid in your javascript
303
+ * anyone can view your source code - nothing sensitive to steal
304
+ * works because authentication happens via browser origin header
305
+
306
+ **browser origin header:**
307
+ * automatically sent by browser on every request
308
+ * contains the domain making the request (e.g., `https://yourdomain.com`)
309
+ * **cannot be spoofed** - browsers prevent javascript from modifying it
310
+ * enforced by browser security model (same-origin policy)
311
+
312
+ **server-side validation:**
313
+ ```
314
+ 1. browser sends request with Origin: https://yourdomain.com
315
+ 2. proxy checks if yourdomain.com is in allowed origins list
316
+ 3. if match: proxy injects api key server-side and calls external api
317
+ 4. if no match: request rejected with 403 error
318
+ 5. api keys never sent to browser
319
+ ```
320
+
321
+ **what if someone steals your code?**
322
+ * they deploy it on their domain (e.g., `https://attacker.com`)
323
+ * browser sends `Origin: https://attacker.com`
324
+ * proxy rejects - their domain not in your origins list
325
+ * api calls fail - they can't use your api keys
326
+
327
+ **comparison to alternatives:**
328
+
329
+ traditional approach (insecure):
330
+ ```javascript
331
+ // api key exposed in code
332
+ const apiKey = 'sk-proj-abc123...';
333
+ fetch('https://api.openai.com/v1/chat/completions', {
334
+ headers: { 'Authorization': `Bearer ${apiKey}` }
335
+ });
336
+ ```
337
+ problem: anyone viewing source code gets your api key
338
+
339
+ environment variables (doesn't work for static sites):
340
+ ```javascript
341
+ // process.env doesn't exist in browser
342
+ const apiKey = process.env.OPENAI_API_KEY; // undefined
343
+ ```
344
+ problem: static sites have no server to hold env vars
345
+
346
+ backend proxy (requires server):
347
+ ```javascript
348
+ // requires deploying and maintaining your own backend
349
+ fetch('https://yourbackend.com/api/openai', { ... });
350
+ ```
351
+ problem: need to deploy and maintain server infrastructure
352
+
353
+ learn-secrets approach (secure for static sites):
354
+ ```javascript
355
+ // zero credentials - authentication via origin header
356
+ const sdk = new SecretsSDK();
357
+ await sdk.post('openai', '/v1/chat/completions', { ... });
358
+ ```
359
+ solution: browser origin header provides authentication
360
+
361
+ ### security guarantees
362
+
363
+ * **api keys never exposed** - stored server-side, injected in proxy layer
364
+ * **origin header enforced** - browsers prevent spoofing, validated server-side
365
+ * **no credentials to leak** - zero-config means no tokens in your code
300
366
  * **rate limiting** - 100 req/min per domain prevents abuse
301
- * **instant updates** - change origins in dashboard anytime
367
+ * **instant revocation** - remove origins in dashboard to block access immediately
368
+ * **encrypted storage** - api keys encrypted at rest in database
369
+ * **https only** - sdk enforces https in production
370
+
371
+ ### best practices
372
+
373
+ **allowed origins:**
374
+ * only add domains you control
375
+ * use specific domains instead of wildcards
376
+ * localhost allowed for development only
377
+ * remove test domains before going live
378
+
379
+ **deployment:**
380
+ * deploy to production domain first
381
+ * then add that domain to allowed origins
382
+ * test that api calls work from production
383
+ * never commit credentials to version control
384
+
385
+ **monitoring:**
386
+ * check usage dashboard regularly
387
+ * watch for unexpected rate limit hits
388
+ * review origins list periodically
389
+ * remove unused origins
390
+
391
+ **incident response:**
392
+ * if domain compromised: remove from origins list immediately
393
+ * if api key leaked: regenerate in provider dashboard
394
+ * if suspecting abuse: check usage logs in dashboard
302
395
 
303
396
  ## documentation
304
397
 
@@ -0,0 +1 @@
1
+ "use strict";var SecretsSDK=(()=>{var h=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var k=(i,e)=>{for(var t in e)h(i,t,{get:e[t],enumerable:!0})},_=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of R(e))!I.call(i,s)&&s!==t&&h(i,s,{get:()=>e[s],enumerable:!(r=T(e,s))||r.enumerable});return i};var S=i=>_(h({},"__esModule",{value:!0}),i);var P={};k(P,{InvalidTokenError:()=>g,OriginMismatchError:()=>c,RateLimitError:()=>u,SecretsSDK:()=>d,SecretsSDKError:()=>n});var n=class extends Error{constructor(t,r,s){super(t);this.status=r;this.response=s;this.name="SecretsSDKError"}},c=class extends n{constructor(e="Origin not allowed for this token"){super(e,403),this.name="OriginMismatchError"}},u=class extends n{constructor(e="Rate limit exceeded",t=60,r=0,s=100){super(e,429),this.name="RateLimitError",this.retryAfter=t,this.remaining=r,this.limit=s}},g=class extends n{constructor(e="Invalid or expired token"){super(e,401),this.name="InvalidTokenError"}};var d=class{constructor(e={}){this.rateLimitInfo=null;this.appId=e.appId||null,this.token=e.token||e.sessionToken||null,this.baseUrl=e.baseUrl||"https://cloudprototype.org",this.timeout=e.timeout||3e4,this.retryOn429=e.retryOn429!==void 0?e.retryOn429:!0,this.zeroConfigMode=!this.appId&&!this.token}getUsage(){return this.rateLimitInfo}parseRateLimitHeaders(e){let t=e.get("X-RateLimit-Limit"),r=e.get("X-RateLimit-Remaining"),s=e.get("X-RateLimit-Reset");t&&r&&s&&(this.rateLimitInfo={limit:parseInt(t),remaining:parseInt(r),reset:parseInt(s)})}sleep(e){return new Promise(t=>setTimeout(t,e))}async fetchWithTimeout(e,t,r){let s=new AbortController,l=setTimeout(()=>s.abort(),r);try{return await fetch(e,{...t,signal:s.signal})}finally{clearTimeout(l)}}async call(e,t,r={}){let s=0,l=this.retryOn429?3:0;for(;;)try{let a=this.zeroConfigMode?`${this.baseUrl}/api/sdk/proxy`:`${this.baseUrl}/api/sdk/${this.appId}/proxy`,f={"Content-Type":"application/json"};!this.zeroConfigMode&&this.token&&(f.Authorization=`Bearer ${this.token}`);let o=await this.fetchWithTimeout(a,{method:"POST",headers:f,body:JSON.stringify({keyName:e,endpoint:t,method:r.method||"GET",body:r.body,headers:r.headers})},this.timeout);this.parseRateLimitHeaders(o.headers);let p=await o.json();if(!o.ok){let m=p.data?.message||p?.message||`Request failed with status ${o.status}`;if(o.status===403&&m.includes("Origin"))throw new c(m);if(o.status===401)throw new g(m);if(o.status===429){let y=this.rateLimitInfo?this.rateLimitInfo.reset-Math.floor(Date.now()/1e3):60,b=new u(m,y,this.rateLimitInfo?.remaining||0,this.rateLimitInfo?.limit||100);if(this.retryOn429&&s<l){s++;let x=Math.min(1e3*Math.pow(2,s-1),8e3);await this.sleep(x);continue}throw b}throw new n(m,o.status,p)}return p.data}catch(a){throw a instanceof n?a:a instanceof Error&&a.name==="AbortError"?new n("Request timeout",408):new n(a instanceof Error?a.message:"Unknown error occurred",500)}}async get(e,t,r){return this.call(e,t,{method:"GET",headers:r})}async post(e,t,r,s){return this.call(e,t,{method:"POST",body:r,headers:s})}async put(e,t,r,s){return this.call(e,t,{method:"PUT",body:r,headers:s})}async delete(e,t,r){return this.call(e,t,{method:"DELETE",headers:r})}async patch(e,t,r,s){return this.call(e,t,{method:"PATCH",body:r,headers:s})}setAppId(e){this.appId=e}async secretsRequest(e,t){if(!this.appId)throw new n("App ID required for secrets management. Call setAppId() first.",400);let r=await this.fetchWithTimeout(`${this.baseUrl}/api/projects/${this.appId}/secrets/manage?action=${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:t?JSON.stringify(t):void 0},this.timeout);if(!r.ok){let s=await r.json().catch(()=>({}));throw new n(s.message||`Secrets management failed: ${r.statusText}`,r.status,s)}return r.json()}async listSecrets(){return this.secretsRequest("list")}async createSecret(e){return this.secretsRequest("create",e)}async updateSecret(e){return this.secretsRequest("update",e)}async deleteSecret(e){return this.secretsRequest("delete",{name:e})}async syncEnv(e,t){return this.secretsRequest("sync",{secrets:e,provider:t})}};return S(P);})();
@@ -0,0 +1,70 @@
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
+ // src/env-resolver.ts
9
+ function resolveEnvString(value) {
10
+ return value.replace(/\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g, (match, var1, var2) => {
11
+ const varName = var1 || var2;
12
+ const envValue = process.env[varName];
13
+ if (envValue === void 0) {
14
+ throw new Error(`Environment variable ${varName} is not defined`);
15
+ }
16
+ return envValue;
17
+ });
18
+ }
19
+ 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
+ function resolveEnvInSecrets(secrets) {
37
+ return secrets.map(resolveEnvInSecret);
38
+ }
39
+ function loadSecretsConfig(configPath) {
40
+ const fs = __require("fs");
41
+ const path = __require("path");
42
+ const fullPath = path.resolve(configPath);
43
+ if (!fs.existsSync(fullPath)) {
44
+ throw new Error(`Config file not found: ${fullPath}`);
45
+ }
46
+ const configData = fs.readFileSync(fullPath, "utf-8");
47
+ const config = JSON.parse(configData);
48
+ if (!config.version) {
49
+ throw new Error('Config file missing "version" field');
50
+ }
51
+ if (!config.project) {
52
+ throw new Error('Config file missing "project" field');
53
+ }
54
+ if (!Array.isArray(config.secrets)) {
55
+ throw new Error('Config file missing "secrets" array');
56
+ }
57
+ const resolvedSecrets = resolveEnvInSecrets(config.secrets);
58
+ return {
59
+ version: config.version,
60
+ project: config.project,
61
+ secrets: resolvedSecrets
62
+ };
63
+ }
64
+
65
+ export {
66
+ resolveEnvString,
67
+ resolveEnvInSecret,
68
+ resolveEnvInSecrets,
69
+ loadSecretsConfig
70
+ };
@@ -772,7 +772,7 @@ Onboarding site for project: ${projectId}`);
772
772
  console.log(`
773
773
  Next steps:`);
774
774
  console.log(` 1. Run: learn-secrets deploy (to update secrets)`);
775
- console.log(` 2. Visit: ${dashboardUrl}/dashboard/secrets to manage secrets`);
775
+ console.log(` 2. Visit: ${dashboardUrl}/dashboard/collections/secrets to manage secrets`);
776
776
  console.log(` 3. View usage at: ${dashboardUrl}/dashboard/settings/usage`);
777
777
  const config = {
778
778
  project: projectId,
@@ -915,18 +915,18 @@ Deploying secrets to project: ${config.project}`);
915
915
  console.log("\nSuccess!");
916
916
  console.log(` Created: ${result.results.created} secrets`);
917
917
  console.log(` Updated: ${result.results.updated} secrets`);
918
+ console.log(` Failed: ${result.results.failed} secrets`);
918
919
  if (result.results.failed > 0) {
919
- console.log(` Failed: ${result.results.failed} secrets`);
920
+ console.log("\nFailed uploads:");
920
921
  for (const failure of result.results.details.failed) {
921
- console.log(` - ${failure.name}: ${failure.error}`);
922
+ console.log(` - ${failure.name}: ${failure.error}`);
922
923
  }
923
924
  }
924
925
  const dashboardUrl = options.baseUrl || "https://cloudprototype.org";
926
+ console.log("\nSecrets deployed successfully!");
925
927
  console.log(`
926
- Secrets deployed to Learn dashboard!`);
927
- console.log(`View at: ${dashboardUrl}/dashboard/collections/secrets`);
928
- console.log(`
929
- Your secrets are now accessible via the Learn Secrets API.`);
928
+ View your secrets in the dashboard:`);
929
+ console.log(` ${dashboardUrl}/dashboard/collections/secrets`);
930
930
  } catch (error) {
931
931
  console.error("\nDeploy failed:", error.message);
932
932
  if (error.status === 401) {
@@ -0,0 +1,12 @@
1
+ import {
2
+ loadSecretsConfig,
3
+ resolveEnvInSecret,
4
+ resolveEnvInSecrets,
5
+ resolveEnvString
6
+ } from "./chunk-Y4RHJLDQ.mjs";
7
+ export {
8
+ loadSecretsConfig,
9
+ resolveEnvInSecret,
10
+ resolveEnvInSecrets,
11
+ resolveEnvString
12
+ };
package/dist/index.mjs CHANGED
@@ -1,10 +1,849 @@
1
- import{a as j,b as A,c as M,d as K}from"./chunk-AS6G7JYX.mjs";var i=class extends Error{constructor(t,r,n){super(t);this.status=r;this.response=n;this.name="SecretsSDKError"}},f=class extends i{constructor(e="Origin not allowed for this token"){super(e,403),this.name="OriginMismatchError"}},y=class extends i{constructor(e="Rate limit exceeded",t=60,r=0,n=100){super(e,429),this.name="RateLimitError",this.retryAfter=t,this.remaining=r,this.limit=n}},w=class extends i{constructor(e="Invalid or expired token"){super(e,401),this.name="InvalidTokenError"}};var T=class{constructor(e={}){this.rateLimitInfo=null;this.appId=e.appId||null,this.token=e.token||e.sessionToken||null,this.baseUrl=e.baseUrl||"https://cloudprototype.org",this.timeout=e.timeout||3e4,this.retryOn429=e.retryOn429!==void 0?e.retryOn429:!0,this.zeroConfigMode=!this.appId&&!this.token}getUsage(){return this.rateLimitInfo}parseRateLimitHeaders(e){let t=e.get("X-RateLimit-Limit"),r=e.get("X-RateLimit-Remaining"),n=e.get("X-RateLimit-Reset");t&&r&&n&&(this.rateLimitInfo={limit:parseInt(t),remaining:parseInt(r),reset:parseInt(n)})}sleep(e){return new Promise(t=>setTimeout(t,e))}async fetchWithTimeout(e,t,r){let n=new AbortController,c=setTimeout(()=>n.abort(),r);try{return await fetch(e,{...t,signal:n.signal})}finally{clearTimeout(c)}}async call(e,t,r={}){let n=0,c=this.retryOn429?3:0;for(;;)try{let o=this.zeroConfigMode?`${this.baseUrl}/api/sdk/proxy`:`${this.baseUrl}/api/sdk/${this.appId}/proxy`,u={"Content-Type":"application/json"};!this.zeroConfigMode&&this.token&&(u.Authorization=`Bearer ${this.token}`);let a=await this.fetchWithTimeout(o,{method:"POST",headers:u,body:JSON.stringify({keyName:e,endpoint:t,method:r.method||"GET",body:r.body,headers:r.headers})},this.timeout);this.parseRateLimitHeaders(a.headers);let h=await a.json();if(!a.ok){let m=h.data?.message||h?.message||`Request failed with status ${a.status}`;if(a.status===403&&m.includes("Origin"))throw new f(m);if(a.status===401)throw new w(m);if(a.status===429){let b=this.rateLimitInfo?this.rateLimitInfo.reset-Math.floor(Date.now()/1e3):60,l=new y(m,b,this.rateLimitInfo?.remaining||0,this.rateLimitInfo?.limit||100);if(this.retryOn429&&n<c){n++;let g=Math.min(1e3*Math.pow(2,n-1),8e3);await this.sleep(g);continue}throw l}throw new i(m,a.status,h)}return h.data}catch(o){throw o instanceof i?o:o instanceof Error&&o.name==="AbortError"?new i("Request timeout",408):new i(o instanceof Error?o.message:"Unknown error occurred",500)}}async get(e,t,r){return this.call(e,t,{method:"GET",headers:r})}async post(e,t,r,n){return this.call(e,t,{method:"POST",body:r,headers:n})}async put(e,t,r,n){return this.call(e,t,{method:"PUT",body:r,headers:n})}async delete(e,t,r){return this.call(e,t,{method:"DELETE",headers:r})}async patch(e,t,r,n){return this.call(e,t,{method:"PATCH",body:r,headers:n})}setAppId(e){this.appId=e}async secretsRequest(e,t){if(!this.appId)throw new i("App ID required for secrets management. Call setAppId() first.",400);let r=await this.fetchWithTimeout(`${this.baseUrl}/api/projects/${this.appId}/secrets/manage?action=${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:t?JSON.stringify(t):void 0},this.timeout);if(!r.ok){let n=await r.json().catch(()=>({}));throw new i(n.message||`Secrets management failed: ${r.statusText}`,r.status,n)}return r.json()}async listSecrets(){return this.secretsRequest("list")}async createSecret(e){return this.secretsRequest("create",e)}async updateSecret(e){return this.secretsRequest("update",e)}async deleteSecret(e){return this.secretsRequest("delete",{name:e})}async syncEnv(e,t){return this.secretsRequest("sync",{secrets:e,provider:t})}};import*as d from"fs";import*as k from"path";import*as C from"os";function v(){let s=C.homedir(),e=k.join(s,".learn");return k.join(e,"credentials.json")}function q(){let s=C.homedir(),e=k.join(s,".learn");d.existsSync(e)||d.mkdirSync(e,{recursive:!0,mode:448})}function I(){try{let s=v();if(!d.existsSync(s))return null;let e=d.readFileSync(s,"utf-8"),t=JSON.parse(e);return t.access_token&&!t["learn-secrets-sdk"]&&!t["learn-auth-sdk"]?{"learn-secrets-sdk":t}:t}catch{return null}}function P(s){q();let e=v(),t=JSON.stringify(s,null,2);d.writeFileSync(e,t,{mode:384})}function S(s){let e=I();return e&&e[s]||null}function R(s,e){let t=I()||{};t[s]=e,P(t)}function N(s){let e=I();e&&(delete e[s],Object.keys(e).length===0?D():P(e))}function _(){return S("learn-secrets-sdk")}function D(){try{let s=v();d.existsSync(s)&&d.unlinkSync(s)}catch{}}function J(s){let e=S(s);return e!==null&&!!e.access_token}function L(){let s=_();return s?s.expires_at?new Date(s.expires_at)>new Date:!0:!1}import{exec as z}from"child_process";import{promisify as F}from"util";import{createHash as U}from"crypto";import*as p from"os";var x=F(z);async function O(){let s=process.platform;try{let e;if(s==="darwin")e=await H();else if(s==="linux")e=await W();else if(s==="win32")e=await G();else throw new Error(`Unsupported platform: ${s}`);let t=U("sha256").update(e).digest("hex").substring(0,32),r=JSON.stringify({machineId:t,cpus:p.cpus().length,arch:p.arch(),platform:p.platform(),homedir:p.homedir()}),n=U("sha256").update(r).digest("hex");return{machineId:t,fingerprint:n}}catch(e){throw new Error(`Failed to get machine ID: ${e.message}`)}}async function H(){try{let{stdout:s}=await x(`ioreg -l | grep IOPlatformSerialNumber | awk '{print $4}' | sed 's/"//g'`),{stdout:e}=await x("hostname"),t=s.trim(),r=e.trim();if(t&&r)return`${t}-${r}`;let{stdout:n}=await x(`ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID | awk '{print $3}' | sed 's/"//g'`);return n.trim()}catch{throw new Error("Failed to get macOS machine ID")}}async function W(){try{let{stdout:s}=await x("cat /etc/machine-id 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null"),e=s.trim();if(e)return e;throw new Error("Machine ID file not found")}catch{throw new Error("Failed to get Linux machine ID")}}async function G(){try{let{stdout:s}=await x("wmic csproduct get uuid"),t=s.split(`
2
- `).map(r=>r.trim()).find(r=>r&&r!=="UUID");if(t)return t;throw new Error("Failed to parse machine UUID from WMIC")}catch{throw new Error("Failed to get Windows machine ID")}}var E=class{constructor(e){this.appId=e.appId,this.baseUrl=e.baseUrl||"https://cloudprototype.org",this.timeout=e.timeout||3e4}async openBrowser(e){let{exec:t}=await import("child_process"),{promisify:r}=await import("util"),n=r(t),c=process.platform;try{c==="darwin"?await n(`open "${e}"`):c==="win32"?await n(`start "${e}"`):await n(`xdg-open "${e}"`)}catch(o){throw console.error("Failed to open browser:",o),new i("Failed to open browser",500)}}sleep(e){return new Promise(t=>setTimeout(t,e))}async login(){let e="learn-secrets-sdk";try{console.log(`Authenticating with Learn (${e})...
3
- `);let t=await O(),r=S(e);if(r&&r.access_token){let l=await fetch(`${this.baseUrl}/api/auth/cli/check-authorization`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({machine_id:t.machineId,sdk_name:e})});if(l.ok){let g=await l.json();if(g.authorized)return console.log("This machine is already authorized."),console.log(`Logged in as: ${g.user_email||g.user_id}`),{success:!0,user:g.user_email||g.user_id}}}let n=await fetch(`${this.baseUrl}/api/auth/cli/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!n.ok)throw new i("Failed to generate device code",n.status);let c=await n.json();console.log(`
4
- Log in on ${this.baseUrl}`),console.log("Login at:"),console.log(c.verification_uri_complete),console.log(`
5
- Press ENTER to open in the browser...`);let u=(await import("readline")).createInterface({input:process.stdin,output:process.stdout});await new Promise(l=>{u.question("",()=>{u.close(),l()})}),console.log(`Opening browser...
6
- `),await this.openBrowser(c.verification_uri_complete);let a=null,h=c.interval*1e3,m=Math.ceil(c.expires_in/c.interval),b=0;for(;b<m;){await this.sleep(h),b++;let l=await fetch(`${this.baseUrl}/api/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:c.device_code})});if(l.status===202){process.stdout.write(".");continue}if(l.ok){a=await l.json();break}}if(!a)throw new i("Device authorization timeout",408);return console.log(`
7
-
8
- Device authorized! Retrieving access token...`),R(e,{access_token:a.access_token,refresh_token:null,expires_at:null,user_id:a.user_id,machine_id:t.machineId,sdk_name:e,authorized_at:new Date().toISOString()}),console.log(`
9
- Authentication successful!`),console.log(`Logged in as: ${a.user_email||a.user_id}`),console.log(`This machine is now permanently authorized.
10
- `),{success:!0,user:a.user_email||a.user_id}}catch(t){throw t instanceof i?t:new i(t.message||"Login failed",t.status||500)}}async isAuthenticated(){return L()}logout(){D(),console.log("Logged out successfully")}getAccessToken(){let e=_();if(!e)throw new i("Not authenticated. Run login() first.",401);if(e.expires_at&&new Date(e.expires_at)<=new Date)throw new i("Token expired. Run login() again.",401);return e.access_token}async request(e,t,r){let n=this.getAccessToken(),c={method:e,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`}};r&&(c.body=JSON.stringify(r));let o=await fetch(`${this.baseUrl}${t}`,c);if(!o.ok){let u=await o.json().catch(()=>({}));throw new i(u.message||`Request failed: ${o.statusText}`,o.status,u)}return o.json()}async list(){return(await this.request("GET",`/api/projects/${this.appId}/secrets`)).secrets}async sync(e,t){return await this.request("POST",`/api/projects/${this.appId}/secrets/sync`,{secrets:e,options:{deleteMissing:t?.deleteMissing??!1,dryRun:t?.dryRun??!1}})}async importSecrets(e){return this.request("POST",`/api/projects/${this.appId}/import-secrets`,{version:e.version||"1.0",origins:e.origins,secrets:e.secrets})}async importFromFile(e){let{loadSecretsConfig:t}=await import("./env-resolver-Y4SFGOKB.mjs"),r=t(e);return this.importSecrets(r)}};var $=class{constructor(e){this.workerUrl=e.workerUrl.replace(/\/$/,"")}async getConfig(){let e=await fetch(`${this.workerUrl}/config`,{method:"GET",credentials:"include"});if(!e.ok)throw new Error(`Failed to get config: ${e.status} ${e.statusText}`);return(await e.json()).public||{}}async proxy(e){let t=await fetch(`${this.workerUrl}/proxy`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(e)});if(!t.ok){let r=await t.json().catch(()=>({error:"Unknown error"}));throw new Error(r.error||`Proxy request failed: ${t.status}`)}return t.json()}async generateAppleJwt(e){let t=await fetch(`${this.workerUrl}/auth/apple-jwt`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(e)});if(!t.ok){let n=await t.json().catch(()=>({error:"Unknown error"}));throw new Error(n.error||`Apple JWT generation failed: ${t.status}`)}return(await t.json()).token}async exchangeOAuthCode(e){let t=await fetch(`${this.workerUrl}/auth/oauth`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(e)});if(!t.ok){let r=await t.json().catch(()=>({error:"Unknown error"}));throw new Error(r.error||`OAuth exchange failed: ${t.status}`)}return t.json()}async health(){let e=await fetch(`${this.workerUrl}/health`,{method:"GET"});if(!e.ok)throw new Error(`Health check failed: ${e.status}`);return e.json()}};export{w as InvalidTokenError,f as OriginMismatchError,y as RateLimitError,E as SecretsManagement,T as SecretsSDK,i as SecretsSDKError,$ as WorkerClient,N as deleteSDKCredentials,O as getMachineId,J as hasSDKCredentials,K as loadSecretsConfig,I as readAllCredentials,S as readSDKCredentials,A as resolveEnvInSecret,M as resolveEnvInSecrets,j as resolveEnvString,P as writeAllCredentials,R as writeSDKCredentials};
1
+ import {
2
+ loadSecretsConfig,
3
+ resolveEnvInSecret,
4
+ resolveEnvInSecrets,
5
+ resolveEnvString
6
+ } from "./chunk-Y4RHJLDQ.mjs";
7
+
8
+ // src/types.ts
9
+ var SecretsSDKError = class extends Error {
10
+ constructor(message, status, response) {
11
+ super(message);
12
+ this.status = status;
13
+ this.response = response;
14
+ this.name = "SecretsSDKError";
15
+ }
16
+ };
17
+ var OriginMismatchError = class extends SecretsSDKError {
18
+ constructor(message = "Origin not allowed for this token") {
19
+ super(message, 403);
20
+ this.name = "OriginMismatchError";
21
+ }
22
+ };
23
+ var RateLimitError = class extends SecretsSDKError {
24
+ constructor(message = "Rate limit exceeded", retryAfter = 60, remaining = 0, limit = 100) {
25
+ super(message, 429);
26
+ this.name = "RateLimitError";
27
+ this.retryAfter = retryAfter;
28
+ this.remaining = remaining;
29
+ this.limit = limit;
30
+ }
31
+ };
32
+ var InvalidTokenError = class extends SecretsSDKError {
33
+ constructor(message = "Invalid or expired token") {
34
+ super(message, 401);
35
+ this.name = "InvalidTokenError";
36
+ }
37
+ };
38
+
39
+ // src/client.ts
40
+ var SecretsSDK = class {
41
+ /**
42
+ * Create a new SecretsSDK instance
43
+ *
44
+ * Zero-config mode (recommended for static sites):
45
+ * const sdk = new SecretsSDK();
46
+ * // Origin header is used for authentication
47
+ *
48
+ * Token mode (for backward compatibility):
49
+ * const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
50
+ */
51
+ constructor(options = {}) {
52
+ this.rateLimitInfo = null;
53
+ this.appId = options.appId || null;
54
+ this.token = options.token || options.sessionToken || null;
55
+ this.baseUrl = options.baseUrl || "https://cloudprototype.org";
56
+ this.timeout = options.timeout || 3e4;
57
+ this.retryOn429 = options.retryOn429 !== void 0 ? options.retryOn429 : true;
58
+ this.zeroConfigMode = !this.appId && !this.token;
59
+ }
60
+ /**
61
+ * Get current rate limit information
62
+ */
63
+ getUsage() {
64
+ return this.rateLimitInfo;
65
+ }
66
+ /**
67
+ * Parse rate limit headers from response
68
+ */
69
+ parseRateLimitHeaders(headers) {
70
+ const limit = headers.get("X-RateLimit-Limit");
71
+ const remaining = headers.get("X-RateLimit-Remaining");
72
+ const reset = headers.get("X-RateLimit-Reset");
73
+ if (limit && remaining && reset) {
74
+ this.rateLimitInfo = {
75
+ limit: parseInt(limit),
76
+ remaining: parseInt(remaining),
77
+ reset: parseInt(reset)
78
+ };
79
+ }
80
+ }
81
+ /**
82
+ * Sleep utility for retry backoff
83
+ */
84
+ sleep(ms) {
85
+ return new Promise((resolve) => setTimeout(resolve, ms));
86
+ }
87
+ /**
88
+ * Make fetch request with timeout
89
+ */
90
+ async fetchWithTimeout(url, options, timeout) {
91
+ const controller = new AbortController();
92
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
93
+ try {
94
+ const response = await fetch(url, {
95
+ ...options,
96
+ signal: controller.signal
97
+ });
98
+ return response;
99
+ } finally {
100
+ clearTimeout(timeoutId);
101
+ }
102
+ }
103
+ /**
104
+ * Call an external API securely through the proxy
105
+ * @param keyName - Name of the API key to use
106
+ * @param endpoint - API endpoint path (e.g., '/v1/chat/completions')
107
+ * @param options - Request options (method, body, headers)
108
+ * @returns Promise with the API response
109
+ */
110
+ async call(keyName, endpoint, options = {}) {
111
+ let retries = 0;
112
+ const maxRetries = this.retryOn429 ? 3 : 0;
113
+ while (true) {
114
+ try {
115
+ const proxyUrl = this.zeroConfigMode ? `${this.baseUrl}/api/sdk/proxy` : `${this.baseUrl}/api/sdk/${this.appId}/proxy`;
116
+ const requestHeaders = {
117
+ "Content-Type": "application/json"
118
+ };
119
+ if (!this.zeroConfigMode && this.token) {
120
+ requestHeaders["Authorization"] = `Bearer ${this.token}`;
121
+ }
122
+ const response = await this.fetchWithTimeout(
123
+ proxyUrl,
124
+ {
125
+ method: "POST",
126
+ headers: requestHeaders,
127
+ body: JSON.stringify({
128
+ keyName,
129
+ endpoint,
130
+ method: options.method || "GET",
131
+ body: options.body,
132
+ headers: options.headers
133
+ })
134
+ },
135
+ this.timeout
136
+ );
137
+ this.parseRateLimitHeaders(response.headers);
138
+ const result = await response.json();
139
+ if (!response.ok) {
140
+ const errorMessage = result.data?.message || result?.message || `Request failed with status ${response.status}`;
141
+ if (response.status === 403 && errorMessage.includes("Origin")) {
142
+ throw new OriginMismatchError(errorMessage);
143
+ }
144
+ if (response.status === 401) {
145
+ throw new InvalidTokenError(errorMessage);
146
+ }
147
+ if (response.status === 429) {
148
+ const retryAfter = this.rateLimitInfo ? this.rateLimitInfo.reset - Math.floor(Date.now() / 1e3) : 60;
149
+ const error = new RateLimitError(
150
+ errorMessage,
151
+ retryAfter,
152
+ this.rateLimitInfo?.remaining || 0,
153
+ this.rateLimitInfo?.limit || 100
154
+ );
155
+ if (this.retryOn429 && retries < maxRetries) {
156
+ retries++;
157
+ const backoffMs = Math.min(1e3 * Math.pow(2, retries - 1), 8e3);
158
+ await this.sleep(backoffMs);
159
+ continue;
160
+ }
161
+ throw error;
162
+ }
163
+ throw new SecretsSDKError(errorMessage, response.status, result);
164
+ }
165
+ return result.data;
166
+ } catch (err) {
167
+ if (err instanceof SecretsSDKError) {
168
+ throw err;
169
+ }
170
+ if (err instanceof Error && err.name === "AbortError") {
171
+ throw new SecretsSDKError("Request timeout", 408);
172
+ }
173
+ throw new SecretsSDKError(
174
+ err instanceof Error ? err.message : "Unknown error occurred",
175
+ 500
176
+ );
177
+ }
178
+ }
179
+ }
180
+ /**
181
+ * Make a GET request
182
+ */
183
+ async get(keyName, endpoint, headers) {
184
+ return this.call(keyName, endpoint, { method: "GET", headers });
185
+ }
186
+ /**
187
+ * Make a POST request
188
+ */
189
+ async post(keyName, endpoint, body, headers) {
190
+ return this.call(keyName, endpoint, { method: "POST", body, headers });
191
+ }
192
+ /**
193
+ * Make a PUT request
194
+ */
195
+ async put(keyName, endpoint, body, headers) {
196
+ return this.call(keyName, endpoint, { method: "PUT", body, headers });
197
+ }
198
+ /**
199
+ * Make a DELETE request
200
+ */
201
+ async delete(keyName, endpoint, headers) {
202
+ return this.call(keyName, endpoint, { method: "DELETE", headers });
203
+ }
204
+ /**
205
+ * Make a PATCH request
206
+ */
207
+ async patch(keyName, endpoint, body, headers) {
208
+ return this.call(keyName, endpoint, { method: "PATCH", body, headers });
209
+ }
210
+ /**
211
+ * Secrets Management API (zero-config mode)
212
+ * These methods allow programmatic CRUD operations on project secrets
213
+ * Requires appId to be set (can be set in constructor or via setAppId)
214
+ */
215
+ /**
216
+ * Set App ID for secrets management
217
+ * Required before calling secrets management methods
218
+ */
219
+ setAppId(appId) {
220
+ this.appId = appId;
221
+ }
222
+ /**
223
+ * Make authenticated request to secrets management API
224
+ */
225
+ async secretsRequest(action, body) {
226
+ if (!this.appId) {
227
+ throw new SecretsSDKError("App ID required for secrets management. Call setAppId() first.", 400);
228
+ }
229
+ const response = await this.fetchWithTimeout(
230
+ `${this.baseUrl}/api/projects/${this.appId}/secrets/manage?action=${action}`,
231
+ {
232
+ method: "POST",
233
+ headers: {
234
+ "Content-Type": "application/json"
235
+ },
236
+ body: body ? JSON.stringify(body) : void 0
237
+ },
238
+ this.timeout
239
+ );
240
+ if (!response.ok) {
241
+ const errorData = await response.json().catch(() => ({}));
242
+ throw new SecretsSDKError(
243
+ errorData.message || `Secrets management failed: ${response.statusText}`,
244
+ response.status,
245
+ errorData
246
+ );
247
+ }
248
+ return response.json();
249
+ }
250
+ /**
251
+ * List all secrets for the project
252
+ * Returns secret metadata (no API keys)
253
+ */
254
+ async listSecrets() {
255
+ return this.secretsRequest("list");
256
+ }
257
+ /**
258
+ * Create a new secret
259
+ */
260
+ async createSecret(secret) {
261
+ return this.secretsRequest("create", secret);
262
+ }
263
+ /**
264
+ * Update an existing secret
265
+ */
266
+ async updateSecret(update) {
267
+ return this.secretsRequest("update", update);
268
+ }
269
+ /**
270
+ * Delete a secret
271
+ */
272
+ async deleteSecret(name) {
273
+ return this.secretsRequest("delete", { name });
274
+ }
275
+ /**
276
+ * Sync secrets from .env file format
277
+ * Bulk upload/update secrets
278
+ */
279
+ async syncEnv(secrets, provider) {
280
+ return this.secretsRequest("sync", { secrets, provider });
281
+ }
282
+ };
283
+
284
+ // src/credentials.ts
285
+ import * as fs from "fs";
286
+ import * as path from "path";
287
+ import * as os from "os";
288
+ function getCredentialsPath() {
289
+ const homeDir = os.homedir();
290
+ const learnDir = path.join(homeDir, ".learn");
291
+ return path.join(learnDir, "credentials.json");
292
+ }
293
+ function ensureLearnDir() {
294
+ const homeDir = os.homedir();
295
+ const learnDir = path.join(homeDir, ".learn");
296
+ if (!fs.existsSync(learnDir)) {
297
+ fs.mkdirSync(learnDir, { recursive: true, mode: 448 });
298
+ }
299
+ }
300
+ function readAllCredentials() {
301
+ try {
302
+ const credPath = getCredentialsPath();
303
+ if (!fs.existsSync(credPath)) {
304
+ return null;
305
+ }
306
+ const data = fs.readFileSync(credPath, "utf-8");
307
+ const store = JSON.parse(data);
308
+ if (store.access_token && !store["learn-secrets-sdk"] && !store["learn-auth-sdk"]) {
309
+ return {
310
+ "learn-secrets-sdk": store
311
+ };
312
+ }
313
+ return store;
314
+ } catch (err) {
315
+ return null;
316
+ }
317
+ }
318
+ function writeAllCredentials(store) {
319
+ ensureLearnDir();
320
+ const credPath = getCredentialsPath();
321
+ const data = JSON.stringify(store, null, 2);
322
+ fs.writeFileSync(credPath, data, { mode: 384 });
323
+ }
324
+ function readSDKCredentials(sdkName) {
325
+ const store = readAllCredentials();
326
+ if (!store) {
327
+ return null;
328
+ }
329
+ return store[sdkName] || null;
330
+ }
331
+ function writeSDKCredentials(sdkName, creds) {
332
+ const store = readAllCredentials() || {};
333
+ store[sdkName] = creds;
334
+ writeAllCredentials(store);
335
+ }
336
+ function deleteSDKCredentials(sdkName) {
337
+ const store = readAllCredentials();
338
+ if (!store) {
339
+ return;
340
+ }
341
+ delete store[sdkName];
342
+ if (Object.keys(store).length === 0) {
343
+ deleteCredentials();
344
+ } else {
345
+ writeAllCredentials(store);
346
+ }
347
+ }
348
+ function readCredentials() {
349
+ return readSDKCredentials("learn-secrets-sdk");
350
+ }
351
+ function deleteCredentials() {
352
+ try {
353
+ const credPath = getCredentialsPath();
354
+ if (fs.existsSync(credPath)) {
355
+ fs.unlinkSync(credPath);
356
+ }
357
+ } catch (err) {
358
+ }
359
+ }
360
+ function hasSDKCredentials(sdkName) {
361
+ const creds = readSDKCredentials(sdkName);
362
+ return creds !== null && !!creds.access_token;
363
+ }
364
+ function hasValidCredentials() {
365
+ const creds = readCredentials();
366
+ if (!creds) {
367
+ return false;
368
+ }
369
+ if (!creds.expires_at) {
370
+ return true;
371
+ }
372
+ const expiresAt = new Date(creds.expires_at);
373
+ const now = /* @__PURE__ */ new Date();
374
+ return expiresAt > now;
375
+ }
376
+
377
+ // src/machine-id.ts
378
+ import { exec } from "child_process";
379
+ import { promisify } from "util";
380
+ import { createHash } from "crypto";
381
+ import * as os2 from "os";
382
+ var execAsync = promisify(exec);
383
+ async function getMachineId() {
384
+ const platform2 = process.platform;
385
+ try {
386
+ let rawId;
387
+ if (platform2 === "darwin") {
388
+ rawId = await getMacOSId();
389
+ } else if (platform2 === "linux") {
390
+ rawId = await getLinuxId();
391
+ } else if (platform2 === "win32") {
392
+ rawId = await getWindowsId();
393
+ } else {
394
+ throw new Error(`Unsupported platform: ${platform2}`);
395
+ }
396
+ const machineId = createHash("sha256").update(rawId).digest("hex").substring(0, 32);
397
+ const fingerprintData = JSON.stringify({
398
+ machineId,
399
+ cpus: os2.cpus().length,
400
+ arch: os2.arch(),
401
+ platform: os2.platform(),
402
+ homedir: os2.homedir()
403
+ });
404
+ const fingerprint = createHash("sha256").update(fingerprintData).digest("hex");
405
+ return { machineId, fingerprint };
406
+ } catch (error) {
407
+ throw new Error(`Failed to get machine ID: ${error.message}`);
408
+ }
409
+ }
410
+ async function getMacOSId() {
411
+ try {
412
+ const { stdout: serial } = await execAsync(
413
+ `ioreg -l | grep IOPlatformSerialNumber | awk '{print $4}' | sed 's/"//g'`
414
+ );
415
+ const { stdout: hostname } = await execAsync("hostname");
416
+ const serialTrimmed = serial.trim();
417
+ const hostnameTrimmed = hostname.trim();
418
+ if (serialTrimmed && hostnameTrimmed) {
419
+ return `${serialTrimmed}-${hostnameTrimmed}`;
420
+ }
421
+ const { stdout: uuid } = await execAsync(
422
+ `ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID | awk '{print $3}' | sed 's/"//g'`
423
+ );
424
+ return uuid.trim();
425
+ } catch (error) {
426
+ throw new Error("Failed to get macOS machine ID");
427
+ }
428
+ }
429
+ async function getLinuxId() {
430
+ try {
431
+ const { stdout } = await execAsync(
432
+ "cat /etc/machine-id 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null"
433
+ );
434
+ const machineId = stdout.trim();
435
+ if (machineId) {
436
+ return machineId;
437
+ }
438
+ throw new Error("Machine ID file not found");
439
+ } catch (error) {
440
+ throw new Error("Failed to get Linux machine ID");
441
+ }
442
+ }
443
+ async function getWindowsId() {
444
+ try {
445
+ const { stdout } = await execAsync("wmic csproduct get uuid");
446
+ const lines = stdout.split("\n").map((l) => l.trim());
447
+ const uuid = lines.find((l) => l && l !== "UUID");
448
+ if (uuid) {
449
+ return uuid;
450
+ }
451
+ throw new Error("Failed to parse machine UUID from WMIC");
452
+ } catch (error) {
453
+ throw new Error("Failed to get Windows machine ID");
454
+ }
455
+ }
456
+
457
+ // src/management.ts
458
+ var SecretsManagement = class {
459
+ constructor(options) {
460
+ this.appId = options.appId;
461
+ this.baseUrl = options.baseUrl || "https://cloudprototype.org";
462
+ this.timeout = options.timeout || 3e4;
463
+ }
464
+ /**
465
+ * Open a URL in the default browser
466
+ */
467
+ async openBrowser(url) {
468
+ const { exec: exec2 } = await import("child_process");
469
+ const { promisify: promisify2 } = await import("util");
470
+ const execAsync2 = promisify2(exec2);
471
+ const platform2 = process.platform;
472
+ try {
473
+ if (platform2 === "darwin") {
474
+ await execAsync2(`open "${url}"`);
475
+ } else if (platform2 === "win32") {
476
+ await execAsync2(`start "${url}"`);
477
+ } else {
478
+ await execAsync2(`xdg-open "${url}"`);
479
+ }
480
+ } catch (err) {
481
+ console.error("Failed to open browser:", err);
482
+ throw new SecretsSDKError("Failed to open browser", 500);
483
+ }
484
+ }
485
+ /**
486
+ * Sleep for specified milliseconds
487
+ */
488
+ sleep(ms) {
489
+ return new Promise((resolve) => setTimeout(resolve, ms));
490
+ }
491
+ /**
492
+ * Browser OAuth login flow with 2FA
493
+ * Opens browser for user to authorize, then prompts for 2FA code
494
+ */
495
+ async login() {
496
+ const SDK_NAME = "learn-secrets-sdk";
497
+ try {
498
+ console.log(`Authenticating with Learn (${SDK_NAME})...
499
+ `);
500
+ const machineInfo = await getMachineId();
501
+ const existingCreds = readSDKCredentials(SDK_NAME);
502
+ if (existingCreds && existingCreds.access_token) {
503
+ const checkResponse = await fetch(`${this.baseUrl}/api/auth/cli/check-authorization`, {
504
+ method: "POST",
505
+ headers: {
506
+ "Content-Type": "application/json"
507
+ },
508
+ body: JSON.stringify({
509
+ machine_id: machineInfo.machineId,
510
+ sdk_name: SDK_NAME
511
+ })
512
+ });
513
+ if (checkResponse.ok) {
514
+ const checkData = await checkResponse.json();
515
+ if (checkData.authorized) {
516
+ console.log("This machine is already authorized.");
517
+ console.log(`Logged in as: ${checkData.user_email || checkData.user_id}`);
518
+ return {
519
+ success: true,
520
+ user: checkData.user_email || checkData.user_id
521
+ };
522
+ }
523
+ }
524
+ }
525
+ const deviceResponse = await fetch(`${this.baseUrl}/api/auth/cli/device`, {
526
+ method: "POST",
527
+ headers: {
528
+ "Content-Type": "application/json"
529
+ }
530
+ });
531
+ if (!deviceResponse.ok) {
532
+ throw new SecretsSDKError(
533
+ "Failed to generate device code",
534
+ deviceResponse.status
535
+ );
536
+ }
537
+ const deviceData = await deviceResponse.json();
538
+ console.log(`
539
+ Log in on ${this.baseUrl}`);
540
+ console.log("Login at:");
541
+ console.log(deviceData.verification_uri_complete);
542
+ console.log("\nPress ENTER to open in the browser...");
543
+ const readline = await import("readline");
544
+ const rl = readline.createInterface({
545
+ input: process.stdin,
546
+ output: process.stdout
547
+ });
548
+ await new Promise((resolve) => {
549
+ rl.question("", () => {
550
+ rl.close();
551
+ resolve();
552
+ });
553
+ });
554
+ console.log("Opening browser...\n");
555
+ await this.openBrowser(deviceData.verification_uri_complete);
556
+ let tokenData = null;
557
+ const interval = deviceData.interval * 1e3;
558
+ const maxAttempts = Math.ceil(deviceData.expires_in / deviceData.interval);
559
+ let attempts = 0;
560
+ while (attempts < maxAttempts) {
561
+ await this.sleep(interval);
562
+ attempts++;
563
+ const tokenResponse = await fetch(`${this.baseUrl}/api/auth/cli/token`, {
564
+ method: "POST",
565
+ headers: {
566
+ "Content-Type": "application/json"
567
+ },
568
+ body: JSON.stringify({
569
+ device_code: deviceData.device_code
570
+ })
571
+ });
572
+ if (tokenResponse.status === 202) {
573
+ process.stdout.write(".");
574
+ continue;
575
+ }
576
+ if (tokenResponse.ok) {
577
+ tokenData = await tokenResponse.json();
578
+ break;
579
+ }
580
+ }
581
+ if (!tokenData) {
582
+ throw new SecretsSDKError("Device authorization timeout", 408);
583
+ }
584
+ console.log("\n\nDevice authorized! Retrieving access token...");
585
+ writeSDKCredentials(SDK_NAME, {
586
+ access_token: tokenData.access_token,
587
+ refresh_token: null,
588
+ expires_at: null,
589
+ user_id: tokenData.user_id,
590
+ machine_id: machineInfo.machineId,
591
+ sdk_name: SDK_NAME,
592
+ authorized_at: (/* @__PURE__ */ new Date()).toISOString()
593
+ });
594
+ console.log("\nAuthentication successful!");
595
+ console.log(`Logged in as: ${tokenData.user_email || tokenData.user_id}`);
596
+ console.log("This machine is now permanently authorized.\n");
597
+ return {
598
+ success: true,
599
+ user: tokenData.user_email || tokenData.user_id
600
+ };
601
+ } catch (err) {
602
+ if (err instanceof SecretsSDKError) {
603
+ throw err;
604
+ }
605
+ throw new SecretsSDKError(
606
+ err.message || "Login failed",
607
+ err.status || 500
608
+ );
609
+ }
610
+ }
611
+ /**
612
+ * Check if user is authenticated
613
+ */
614
+ async isAuthenticated() {
615
+ return hasValidCredentials();
616
+ }
617
+ /**
618
+ * Logout - clear stored credentials
619
+ */
620
+ logout() {
621
+ deleteCredentials();
622
+ console.log("Logged out successfully");
623
+ }
624
+ /**
625
+ * Get access token from stored credentials
626
+ * Throws error if not authenticated
627
+ */
628
+ getAccessToken() {
629
+ const creds = readCredentials();
630
+ if (!creds) {
631
+ throw new SecretsSDKError("Not authenticated. Run login() first.", 401);
632
+ }
633
+ if (creds.expires_at) {
634
+ const expiresAt = new Date(creds.expires_at);
635
+ const now = /* @__PURE__ */ new Date();
636
+ if (expiresAt <= now) {
637
+ throw new SecretsSDKError("Token expired. Run login() again.", 401);
638
+ }
639
+ }
640
+ return creds.access_token;
641
+ }
642
+ /**
643
+ * Make authenticated request
644
+ */
645
+ async request(method, path2, body) {
646
+ const token = this.getAccessToken();
647
+ const options = {
648
+ method,
649
+ headers: {
650
+ "Content-Type": "application/json",
651
+ Authorization: `Bearer ${token}`
652
+ }
653
+ };
654
+ if (body) {
655
+ options.body = JSON.stringify(body);
656
+ }
657
+ const response = await fetch(`${this.baseUrl}${path2}`, options);
658
+ if (!response.ok) {
659
+ const errorData = await response.json().catch(() => ({}));
660
+ throw new SecretsSDKError(
661
+ errorData.message || `Request failed: ${response.statusText}`,
662
+ response.status,
663
+ errorData
664
+ );
665
+ }
666
+ return response.json();
667
+ }
668
+ /**
669
+ * List all secrets (with masked API keys)
670
+ */
671
+ async list() {
672
+ const response = await this.request(
673
+ "GET",
674
+ `/api/projects/${this.appId}/secrets`
675
+ );
676
+ return response.secrets;
677
+ }
678
+ /**
679
+ * Sync secrets from config
680
+ */
681
+ async sync(secrets, options) {
682
+ const response = await this.request(
683
+ "POST",
684
+ `/api/projects/${this.appId}/secrets/sync`,
685
+ {
686
+ secrets,
687
+ options: {
688
+ deleteMissing: options?.deleteMissing ?? false,
689
+ dryRun: options?.dryRun ?? false
690
+ }
691
+ }
692
+ );
693
+ return response;
694
+ }
695
+ /**
696
+ * Import secrets from a config object (bulk import)
697
+ * Also creates SDK token if origins are provided
698
+ *
699
+ * @param config - The secrets configuration
700
+ * @returns Import results including created/updated counts and optional SDK token
701
+ */
702
+ async importSecrets(config) {
703
+ return this.request(
704
+ "POST",
705
+ `/api/projects/${this.appId}/import-secrets`,
706
+ {
707
+ version: config.version || "1.0",
708
+ origins: config.origins,
709
+ secrets: config.secrets
710
+ }
711
+ );
712
+ }
713
+ /**
714
+ * Import secrets from a JSON file path
715
+ * Resolves environment variables in the config
716
+ */
717
+ async importFromFile(configPath) {
718
+ const { loadSecretsConfig: loadSecretsConfig2 } = await import("./env-resolver-FQMR3R4G.mjs");
719
+ const config = loadSecretsConfig2(configPath);
720
+ return this.importSecrets(config);
721
+ }
722
+ };
723
+
724
+ // src/worker-client.ts
725
+ var WorkerClient = class {
726
+ constructor(options) {
727
+ this.workerUrl = options.workerUrl.replace(/\/$/, "");
728
+ }
729
+ /**
730
+ * Get public configuration
731
+ *
732
+ * Returns only PUBLIC_* environment variables from Worker
733
+ * Safe for frontend use
734
+ */
735
+ async getConfig() {
736
+ const response = await fetch(`${this.workerUrl}/config`, {
737
+ method: "GET",
738
+ credentials: "include"
739
+ });
740
+ if (!response.ok) {
741
+ throw new Error(`Failed to get config: ${response.status} ${response.statusText}`);
742
+ }
743
+ const data = await response.json();
744
+ return data.public || {};
745
+ }
746
+ /**
747
+ * Proxy API request with secret injection
748
+ *
749
+ * Makes API call through Worker which injects secret server-side
750
+ * Secret never reaches browser
751
+ *
752
+ * @param request - Proxy request configuration
753
+ * @returns API response data
754
+ */
755
+ async proxy(request) {
756
+ const response = await fetch(`${this.workerUrl}/proxy`, {
757
+ method: "POST",
758
+ headers: {
759
+ "Content-Type": "application/json"
760
+ },
761
+ credentials: "include",
762
+ body: JSON.stringify(request)
763
+ });
764
+ if (!response.ok) {
765
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
766
+ throw new Error(error.error || `Proxy request failed: ${response.status}`);
767
+ }
768
+ return response.json();
769
+ }
770
+ /**
771
+ * Generate Apple Music JWT
772
+ *
773
+ * Private key stays in Worker, only JWT returned to frontend
774
+ *
775
+ * @param request - Apple JWT request with secret name
776
+ * @returns JWT token
777
+ */
778
+ async generateAppleJwt(request) {
779
+ const response = await fetch(`${this.workerUrl}/auth/apple-jwt`, {
780
+ method: "POST",
781
+ headers: {
782
+ "Content-Type": "application/json"
783
+ },
784
+ credentials: "include",
785
+ body: JSON.stringify(request)
786
+ });
787
+ if (!response.ok) {
788
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
789
+ throw new Error(error.error || `Apple JWT generation failed: ${response.status}`);
790
+ }
791
+ const data = await response.json();
792
+ return data.token;
793
+ }
794
+ /**
795
+ * Exchange OAuth authorization code for access token
796
+ *
797
+ * Client secret stays in Worker, tokens returned to frontend
798
+ *
799
+ * @param request - OAuth exchange request
800
+ * @returns OAuth token response
801
+ */
802
+ async exchangeOAuthCode(request) {
803
+ const response = await fetch(`${this.workerUrl}/auth/oauth`, {
804
+ method: "POST",
805
+ headers: {
806
+ "Content-Type": "application/json"
807
+ },
808
+ credentials: "include",
809
+ body: JSON.stringify(request)
810
+ });
811
+ if (!response.ok) {
812
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
813
+ throw new Error(error.error || `OAuth exchange failed: ${response.status}`);
814
+ }
815
+ return response.json();
816
+ }
817
+ /**
818
+ * Health check
819
+ */
820
+ async health() {
821
+ const response = await fetch(`${this.workerUrl}/health`, {
822
+ method: "GET"
823
+ });
824
+ if (!response.ok) {
825
+ throw new Error(`Health check failed: ${response.status}`);
826
+ }
827
+ return response.json();
828
+ }
829
+ };
830
+ export {
831
+ InvalidTokenError,
832
+ OriginMismatchError,
833
+ RateLimitError,
834
+ SecretsManagement,
835
+ SecretsSDK,
836
+ SecretsSDKError,
837
+ WorkerClient,
838
+ deleteSDKCredentials,
839
+ getMachineId,
840
+ hasSDKCredentials,
841
+ loadSecretsConfig,
842
+ readAllCredentials,
843
+ readSDKCredentials,
844
+ resolveEnvInSecret,
845
+ resolveEnvInSecrets,
846
+ resolveEnvString,
847
+ writeAllCredentials,
848
+ writeSDKCredentials
849
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "learn-secrets-sdk",
3
- "version": "1.11.1",
3
+ "version": "1.11.3",
4
4
  "description": "Secrets management SDK for static sites with Learn platform",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -39,7 +39,7 @@
39
39
  "typescript": "^5.0.0"
40
40
  },
41
41
  "scripts": {
42
- "build": "tsup src/index.ts --format esm,iife --dts --minify --global-name SecretsSDK && tsup src/cli/index.ts --format esm --dts --out-dir dist/cli --external commander",
42
+ "build": "tsup src/index.ts --format esm --dts && tsup src/browser.ts --format iife --minify --global-name SecretsSDK && tsup src/cli/index.ts --format esm --dts --out-dir dist/cli --external commander",
43
43
  "prepublishOnly": "npm run build"
44
44
  }
45
45
  }