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 +97 -4
- package/dist/browser.global.js +1 -0
- package/dist/chunk-Y4RHJLDQ.mjs +70 -0
- package/dist/cli/index.mjs +7 -7
- package/dist/env-resolver-FQMR3R4G.mjs +12 -0
- package/dist/index.mjs +849 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -294,11 +294,104 @@ custom providers supported - configure base url in dashboard.
|
|
|
294
294
|
|
|
295
295
|
## security
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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
|
+
};
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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(
|
|
920
|
+
console.log("\nFailed uploads:");
|
|
920
921
|
for (const failure of result.results.details.failed) {
|
|
921
|
-
console.log(`
|
|
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
|
-
|
|
927
|
-
console.log(`
|
|
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) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,849 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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
|
|
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
|
}
|