learn-secrets-sdk 1.10.0 → 1.11.2
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 +13 -329
- package/dist/env-resolver-FQMR3R4G.mjs +12 -0
- package/dist/index.mjs +849 -10
- package/package.json +3 -3
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
|
@@ -362,7 +362,7 @@ Log in on ${this.baseUrl}`);
|
|
|
362
362
|
/**
|
|
363
363
|
* Make authenticated request
|
|
364
364
|
*/
|
|
365
|
-
async request(method,
|
|
365
|
+
async request(method, path2, body) {
|
|
366
366
|
const token = this.getAccessToken();
|
|
367
367
|
const options = {
|
|
368
368
|
method,
|
|
@@ -374,7 +374,7 @@ Log in on ${this.baseUrl}`);
|
|
|
374
374
|
if (body) {
|
|
375
375
|
options.body = JSON.stringify(body);
|
|
376
376
|
}
|
|
377
|
-
const response = await fetch(`${this.baseUrl}${
|
|
377
|
+
const response = await fetch(`${this.baseUrl}${path2}`, options);
|
|
378
378
|
if (!response.ok) {
|
|
379
379
|
const errorData = await response.json().catch(() => ({}));
|
|
380
380
|
throw new SecretsSDKError(
|
|
@@ -675,107 +675,6 @@ function summarizeDetectedSecrets(secrets) {
|
|
|
675
675
|
return lines.join("\n");
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
-
// src/cli/commands/connect-cloudflare.ts
|
|
679
|
-
function getApiUrl(baseUrl) {
|
|
680
|
-
return baseUrl || "https://cloudprototype.org";
|
|
681
|
-
}
|
|
682
|
-
async function connectCloudflare(apiToken, baseUrl) {
|
|
683
|
-
if (!apiToken.match(/^[A-Za-z0-9_-]{40,}$/)) {
|
|
684
|
-
return {
|
|
685
|
-
success: false,
|
|
686
|
-
error: "Invalid API token format. Token should be 40+ alphanumeric characters"
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
const validation = await validateCloudflareToken(apiToken);
|
|
690
|
-
if (!validation.valid) {
|
|
691
|
-
return {
|
|
692
|
-
success: false,
|
|
693
|
-
error: validation.error || "Token validation failed"
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
try {
|
|
697
|
-
const apiUrl = getApiUrl(baseUrl);
|
|
698
|
-
const response = await fetch(`${apiUrl}/api/cloudflare/connect`, {
|
|
699
|
-
method: "POST",
|
|
700
|
-
headers: {
|
|
701
|
-
"Content-Type": "application/json"
|
|
702
|
-
},
|
|
703
|
-
credentials: "include",
|
|
704
|
-
body: JSON.stringify({
|
|
705
|
-
apiToken,
|
|
706
|
-
accountId: validation.accountId,
|
|
707
|
-
accountName: validation.accountName
|
|
708
|
-
})
|
|
709
|
-
});
|
|
710
|
-
if (!response.ok) {
|
|
711
|
-
const error = await response.json();
|
|
712
|
-
return {
|
|
713
|
-
success: false,
|
|
714
|
-
error: error.message || "Failed to store token"
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
return {
|
|
718
|
-
success: true,
|
|
719
|
-
accountId: validation.accountId,
|
|
720
|
-
accountName: validation.accountName
|
|
721
|
-
};
|
|
722
|
-
} catch (error) {
|
|
723
|
-
return {
|
|
724
|
-
success: false,
|
|
725
|
-
error: error.message || "Failed to connect Cloudflare account"
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async function validateCloudflareToken(token) {
|
|
730
|
-
try {
|
|
731
|
-
const response = await fetch("https://api.cloudflare.com/client/v4/user", {
|
|
732
|
-
headers: {
|
|
733
|
-
Authorization: `Bearer ${token}`,
|
|
734
|
-
"Content-Type": "application/json"
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
if (!response.ok) {
|
|
738
|
-
const error = await response.json();
|
|
739
|
-
return {
|
|
740
|
-
valid: false,
|
|
741
|
-
error: error.errors?.[0]?.message || "Invalid token"
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
const data = await response.json();
|
|
745
|
-
const accountsResponse = await fetch("https://api.cloudflare.com/client/v4/accounts", {
|
|
746
|
-
headers: {
|
|
747
|
-
Authorization: `Bearer ${token}`,
|
|
748
|
-
"Content-Type": "application/json"
|
|
749
|
-
}
|
|
750
|
-
});
|
|
751
|
-
if (!accountsResponse.ok) {
|
|
752
|
-
return {
|
|
753
|
-
valid: false,
|
|
754
|
-
error: "Token does not have access to accounts"
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
const accountsData = await accountsResponse.json();
|
|
758
|
-
const accounts = accountsData.result || [];
|
|
759
|
-
if (accounts.length === 0) {
|
|
760
|
-
return {
|
|
761
|
-
valid: false,
|
|
762
|
-
error: "No accounts found for this token"
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
const account = accounts[0];
|
|
766
|
-
return {
|
|
767
|
-
valid: true,
|
|
768
|
-
accountId: account.id,
|
|
769
|
-
accountName: account.name
|
|
770
|
-
};
|
|
771
|
-
} catch (error) {
|
|
772
|
-
return {
|
|
773
|
-
valid: false,
|
|
774
|
-
error: error.message || "Failed to validate token"
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
678
|
// src/cli/commands/init.ts
|
|
780
679
|
async function initCommand(options) {
|
|
781
680
|
if (!hasValidCredentials()) {
|
|
@@ -865,67 +764,16 @@ Onboarding site for project: ${projectId}`);
|
|
|
865
764
|
console.log(` - ${failure.name}: ${failure.error}`);
|
|
866
765
|
}
|
|
867
766
|
}
|
|
767
|
+
const dashboardUrl = options.baseUrl || "https://cloudprototype.org";
|
|
868
768
|
console.log("\nYour site is ready!");
|
|
869
769
|
console.log("\nAdd to your static site code:");
|
|
870
770
|
console.log(" const sdk = new SecretsSDK();");
|
|
871
771
|
console.log(" // Uses origin-based auth - no token needed!");
|
|
872
|
-
console.log("\n--- Cloudflare Worker Setup ---");
|
|
873
|
-
const dashboardUrl = options.baseUrl || "https://cloudprototype.org";
|
|
874
|
-
let cloudflareConnected = false;
|
|
875
|
-
try {
|
|
876
|
-
const statusResponse = await fetch(`${dashboardUrl}/api/cloudflare/status`, {
|
|
877
|
-
credentials: "include"
|
|
878
|
-
});
|
|
879
|
-
cloudflareConnected = statusResponse.ok;
|
|
880
|
-
} catch {
|
|
881
|
-
cloudflareConnected = false;
|
|
882
|
-
}
|
|
883
|
-
if (cloudflareConnected) {
|
|
884
|
-
console.log("Cloudflare account is already connected.");
|
|
885
|
-
} else {
|
|
886
|
-
console.log("To deploy secrets via Cloudflare Worker, we need your Cloudflare API token.");
|
|
887
|
-
console.log("\nRequired permissions:");
|
|
888
|
-
console.log(" - Workers Scripts: Edit");
|
|
889
|
-
console.log(" - Workers Routes: Edit");
|
|
890
|
-
console.log(" - Account Settings: Read");
|
|
891
|
-
console.log("\nCreate token at: https://dash.cloudflare.com/profile/api-tokens");
|
|
892
|
-
const readline = await import("readline");
|
|
893
|
-
const rl = readline.createInterface({
|
|
894
|
-
input: process.stdin,
|
|
895
|
-
output: process.stdout
|
|
896
|
-
});
|
|
897
|
-
const apiToken = await new Promise((resolve2) => {
|
|
898
|
-
rl.question("\nCloudflare API Token (or press Enter to skip): ", (answer) => {
|
|
899
|
-
rl.close();
|
|
900
|
-
resolve2(answer.trim());
|
|
901
|
-
});
|
|
902
|
-
});
|
|
903
|
-
if (apiToken) {
|
|
904
|
-
console.log("\nValidating token...");
|
|
905
|
-
const result2 = await connectCloudflare(apiToken, options.baseUrl);
|
|
906
|
-
if (result2.success) {
|
|
907
|
-
console.log("Cloudflare account connected");
|
|
908
|
-
console.log(` Account: ${result2.accountName}`);
|
|
909
|
-
cloudflareConnected = true;
|
|
910
|
-
} else {
|
|
911
|
-
console.log(`
|
|
912
|
-
Warning: Failed to connect Cloudflare: ${result2.error}`);
|
|
913
|
-
console.log("You can connect later by running: learn-secrets deploy");
|
|
914
|
-
}
|
|
915
|
-
} else {
|
|
916
|
-
console.log("\nSkipped Cloudflare connection.");
|
|
917
|
-
console.log("You can connect later when you run: learn-secrets deploy");
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
772
|
console.log(`
|
|
921
773
|
Next steps:`);
|
|
922
|
-
console.log(` 1. Run: learn-secrets deploy`);
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
} else {
|
|
926
|
-
console.log(` (Connect Cloudflare and deploy Worker with your secrets)`);
|
|
927
|
-
}
|
|
928
|
-
console.log(` 2. Visit: ${dashboardUrl}/dashboard to manage secrets`);
|
|
774
|
+
console.log(` 1. Run: learn-secrets deploy (to update secrets)`);
|
|
775
|
+
console.log(` 2. Visit: ${dashboardUrl}/dashboard/secrets to manage secrets`);
|
|
776
|
+
console.log(` 3. View usage at: ${dashboardUrl}/dashboard/settings/usage`);
|
|
929
777
|
const config = {
|
|
930
778
|
project: projectId,
|
|
931
779
|
origins,
|
|
@@ -994,87 +842,6 @@ Configuration saved to: ${configFile}`);
|
|
|
994
842
|
|
|
995
843
|
// src/cli/commands/deploy.ts
|
|
996
844
|
import fs5 from "fs";
|
|
997
|
-
|
|
998
|
-
// src/cli/commands/provision-worker.ts
|
|
999
|
-
import path2 from "path";
|
|
1000
|
-
import { fileURLToPath } from "url";
|
|
1001
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
1002
|
-
var __dirname = path2.dirname(__filename);
|
|
1003
|
-
function getApiUrl2(baseUrl) {
|
|
1004
|
-
return baseUrl || "https://cloudprototype.org";
|
|
1005
|
-
}
|
|
1006
|
-
async function provisionWorker(workerName, origins, envVars, baseUrl) {
|
|
1007
|
-
const publicVars = [];
|
|
1008
|
-
const secretVars = [];
|
|
1009
|
-
for (const { key, value } of envVars) {
|
|
1010
|
-
if (isPublicConfig(key)) {
|
|
1011
|
-
publicVars.push({ key, value });
|
|
1012
|
-
} else {
|
|
1013
|
-
secretVars.push({ key, value });
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
try {
|
|
1017
|
-
const apiUrl = getApiUrl2(baseUrl);
|
|
1018
|
-
const deployResponse = await fetch(`${apiUrl}/api/cloudflare/provision-worker`, {
|
|
1019
|
-
method: "POST",
|
|
1020
|
-
headers: {
|
|
1021
|
-
"Content-Type": "application/json"
|
|
1022
|
-
},
|
|
1023
|
-
credentials: "include",
|
|
1024
|
-
body: JSON.stringify({
|
|
1025
|
-
workerName,
|
|
1026
|
-
allowedOrigins: origins,
|
|
1027
|
-
publicVars: publicVars.map((v) => ({
|
|
1028
|
-
key: `PUBLIC_${v.key}`,
|
|
1029
|
-
value: v.value
|
|
1030
|
-
})),
|
|
1031
|
-
secrets: secretVars.map((v) => ({
|
|
1032
|
-
key: `SECRET_${v.key}`,
|
|
1033
|
-
value: v.value
|
|
1034
|
-
}))
|
|
1035
|
-
})
|
|
1036
|
-
});
|
|
1037
|
-
if (!deployResponse.ok) {
|
|
1038
|
-
const error = await deployResponse.json();
|
|
1039
|
-
return {
|
|
1040
|
-
success: false,
|
|
1041
|
-
error: error.message || "Failed to deploy Worker"
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
const result = await deployResponse.json();
|
|
1045
|
-
return {
|
|
1046
|
-
success: true,
|
|
1047
|
-
workerUrl: result.workerUrl,
|
|
1048
|
-
workerName: result.workerName
|
|
1049
|
-
};
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
return {
|
|
1052
|
-
success: false,
|
|
1053
|
-
error: error.message || "Failed to provision Worker"
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
function isPublicConfig(key) {
|
|
1058
|
-
const publicPatterns = [
|
|
1059
|
-
/_DOMAIN$/,
|
|
1060
|
-
/_CLIENT_ID$/,
|
|
1061
|
-
/_AUDIENCE$/,
|
|
1062
|
-
/_CALLBACK_URL$/,
|
|
1063
|
-
/_ISSUER$/,
|
|
1064
|
-
/_TEAM_ID$/,
|
|
1065
|
-
/_KEY_ID$/,
|
|
1066
|
-
/^API_BASE_URL$/,
|
|
1067
|
-
/^PUBLIC_/
|
|
1068
|
-
];
|
|
1069
|
-
for (const pattern of publicPatterns) {
|
|
1070
|
-
if (pattern.test(key)) {
|
|
1071
|
-
return true;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
return false;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// src/cli/commands/deploy.ts
|
|
1078
845
|
async function deployCommand(options = {}) {
|
|
1079
846
|
if (!hasValidCredentials()) {
|
|
1080
847
|
console.error('Error: Not authenticated. Run "learn-secrets login" first.');
|
|
@@ -1148,101 +915,18 @@ Deploying secrets to project: ${config.project}`);
|
|
|
1148
915
|
console.log("\nSuccess!");
|
|
1149
916
|
console.log(` Created: ${result.results.created} secrets`);
|
|
1150
917
|
console.log(` Updated: ${result.results.updated} secrets`);
|
|
918
|
+
console.log(` Failed: ${result.results.failed} secrets`);
|
|
1151
919
|
if (result.results.failed > 0) {
|
|
1152
|
-
console.log(
|
|
920
|
+
console.log("\nFailed uploads:");
|
|
1153
921
|
for (const failure of result.results.details.failed) {
|
|
1154
|
-
console.log(`
|
|
922
|
+
console.log(` - ${failure.name}: ${failure.error}`);
|
|
1155
923
|
}
|
|
1156
924
|
}
|
|
1157
925
|
const dashboardUrl = options.baseUrl || "https://cloudprototype.org";
|
|
926
|
+
console.log("\nSecrets deployed successfully!");
|
|
1158
927
|
console.log(`
|
|
1159
|
-
|
|
1160
|
-
console.log(
|
|
1161
|
-
let cloudflareConnected = false;
|
|
1162
|
-
try {
|
|
1163
|
-
const statusResponse = await fetch(`${dashboardUrl}/api/cloudflare/status`, {
|
|
1164
|
-
credentials: "include"
|
|
1165
|
-
});
|
|
1166
|
-
cloudflareConnected = statusResponse.ok;
|
|
1167
|
-
} catch {
|
|
1168
|
-
cloudflareConnected = false;
|
|
1169
|
-
}
|
|
1170
|
-
if (!cloudflareConnected) {
|
|
1171
|
-
console.log("Cloudflare account not connected.");
|
|
1172
|
-
console.log("\nTo deploy Worker, we need your Cloudflare API token.");
|
|
1173
|
-
console.log("Required permissions:");
|
|
1174
|
-
console.log(" - Workers Scripts: Edit");
|
|
1175
|
-
console.log(" - Workers Routes: Edit");
|
|
1176
|
-
console.log(" - Account Settings: Read");
|
|
1177
|
-
console.log("\nCreate token at: https://dash.cloudflare.com/profile/api-tokens");
|
|
1178
|
-
const readline = await import("readline");
|
|
1179
|
-
const rl = readline.createInterface({
|
|
1180
|
-
input: process.stdin,
|
|
1181
|
-
output: process.stdout
|
|
1182
|
-
});
|
|
1183
|
-
const apiToken = await new Promise((resolve2) => {
|
|
1184
|
-
rl.question("\nCloudflare API Token (or press Enter to skip): ", (answer) => {
|
|
1185
|
-
rl.close();
|
|
1186
|
-
resolve2(answer.trim());
|
|
1187
|
-
});
|
|
1188
|
-
});
|
|
1189
|
-
if (apiToken) {
|
|
1190
|
-
console.log("\nValidating token...");
|
|
1191
|
-
const result2 = await connectCloudflare(apiToken, options.baseUrl);
|
|
1192
|
-
if (result2.success) {
|
|
1193
|
-
console.log("Cloudflare account connected");
|
|
1194
|
-
console.log(` Account: ${result2.accountName}`);
|
|
1195
|
-
cloudflareConnected = true;
|
|
1196
|
-
} else {
|
|
1197
|
-
console.log(`
|
|
1198
|
-
Error: Failed to connect Cloudflare: ${result2.error}`);
|
|
1199
|
-
console.log("Worker deployment skipped.");
|
|
1200
|
-
}
|
|
1201
|
-
} else {
|
|
1202
|
-
console.log("\nSkipped Cloudflare connection. Worker deployment skipped.");
|
|
1203
|
-
}
|
|
1204
|
-
} else {
|
|
1205
|
-
console.log("Cloudflare account is connected.");
|
|
1206
|
-
}
|
|
1207
|
-
if (cloudflareConnected) {
|
|
1208
|
-
const readline = await import("readline");
|
|
1209
|
-
const rl = readline.createInterface({
|
|
1210
|
-
input: process.stdin,
|
|
1211
|
-
output: process.stdout
|
|
1212
|
-
});
|
|
1213
|
-
const shouldProvision = await new Promise((resolve2) => {
|
|
1214
|
-
rl.question("\nDeploy/update Worker to Cloudflare? (y/N): ", (answer) => {
|
|
1215
|
-
rl.close();
|
|
1216
|
-
resolve2(answer.trim());
|
|
1217
|
-
});
|
|
1218
|
-
});
|
|
1219
|
-
if (shouldProvision.toLowerCase() === "y" || shouldProvision.toLowerCase() === "yes") {
|
|
1220
|
-
console.log("\nDeploying Worker to Cloudflare...");
|
|
1221
|
-
const workerName = `learn-secrets-${config.project.toLowerCase()}`;
|
|
1222
|
-
const provisionResult = await provisionWorker(
|
|
1223
|
-
workerName,
|
|
1224
|
-
config.origins,
|
|
1225
|
-
envVars,
|
|
1226
|
-
options.baseUrl
|
|
1227
|
-
);
|
|
1228
|
-
if (provisionResult.success) {
|
|
1229
|
-
console.log("\nWorker deployed successfully");
|
|
1230
|
-
console.log(` Name: ${provisionResult.workerName}`);
|
|
1231
|
-
console.log(` URL: ${provisionResult.workerUrl}`);
|
|
1232
|
-
config.workerUrl = provisionResult.workerUrl;
|
|
1233
|
-
fs5.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
1234
|
-
console.log("\nNext steps:");
|
|
1235
|
-
console.log(` 1. Test Worker: curl ${provisionResult.workerUrl}/health`);
|
|
1236
|
-
console.log(` 2. Update your frontend to use Worker URL`);
|
|
1237
|
-
console.log(` 3. Test origin validation from your site`);
|
|
1238
|
-
} else {
|
|
1239
|
-
console.log(`
|
|
1240
|
-
Error: Worker deployment failed: ${provisionResult.error}`);
|
|
1241
|
-
}
|
|
1242
|
-
} else {
|
|
1243
|
-
console.log("\nSkipped Worker deployment.");
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
928
|
+
View your secrets in the dashboard:`);
|
|
929
|
+
console.log(` ${dashboardUrl}/dashboard/collections/secrets`);
|
|
1246
930
|
} catch (error) {
|
|
1247
931
|
console.error("\nDeploy failed:", error.message);
|
|
1248
932
|
if (error.status === 401) {
|
|
@@ -1257,7 +941,7 @@ Error: Worker deployment failed: ${provisionResult.error}`);
|
|
|
1257
941
|
|
|
1258
942
|
// src/cli/index.ts
|
|
1259
943
|
var program = new Command();
|
|
1260
|
-
program.name("learn-secrets").description("CLI tool for managing
|
|
944
|
+
program.name("learn-secrets").description("CLI tool for managing secrets with the Learn platform").version("1.11.1");
|
|
1261
945
|
program.command("login").description("Authenticate with Learn Secrets via browser").option("--base-url <url>", "Custom base URL for API").action(async (options) => {
|
|
1262
946
|
await loginCommand(options);
|
|
1263
947
|
});
|