learn-secrets-sdk 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,385 @@
1
- export { SecretsSDK } from './client';
2
- export { SecretsManagement } from './management';
3
- export { resolveEnvString, resolveEnvInSecret, resolveEnvInSecrets, loadSecretsConfig } from './env-resolver';
4
- export { SecretsSDKError, OriginMismatchError, RateLimitError, InvalidTokenError } from './types';
5
- export type { SecretsSDKOptions, ProxyRequest, ProxyResponse, RateLimitInfo, SecretsManagementOptions, SecretConfig, MaskedSecret, SyncOptions, SyncResult, CLICredentials } from './types';
1
+ interface SecretsSDKOptions {
2
+ /**
3
+ * App ID from dashboard (optional in zero-config mode)
4
+ * If omitted, SDK uses origin-based authentication
5
+ */
6
+ appId?: string;
7
+ /**
8
+ * SDK token (optional in zero-config mode)
9
+ * If omitted, SDK uses origin-based authentication
10
+ */
11
+ token?: string;
12
+ sessionToken?: string;
13
+ /**
14
+ * Base URL of the proxy server
15
+ * Default: https://ctklearn.carsontkempf.workers.dev
16
+ */
17
+ baseUrl?: string;
18
+ timeout?: number;
19
+ retryOn429?: boolean;
20
+ }
21
+ interface ProxyRequest {
22
+ keyName: string;
23
+ endpoint: string;
24
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
25
+ body?: any;
26
+ headers?: Record<string, string>;
27
+ }
28
+ interface ProxyResponse<T = any> {
29
+ success: boolean;
30
+ status: number;
31
+ data: T;
32
+ }
33
+ interface RateLimitInfo {
34
+ limit: number;
35
+ remaining: number;
36
+ reset: number;
37
+ }
38
+ declare class SecretsSDKError extends Error {
39
+ status: number;
40
+ response?: any | undefined;
41
+ constructor(message: string, status: number, response?: any | undefined);
42
+ }
43
+ declare class OriginMismatchError extends SecretsSDKError {
44
+ constructor(message?: string);
45
+ }
46
+ declare class RateLimitError extends SecretsSDKError {
47
+ retryAfter: number;
48
+ remaining: number;
49
+ limit: number;
50
+ constructor(message?: string, retryAfter?: number, remaining?: number, limit?: number);
51
+ }
52
+ declare class InvalidTokenError extends SecretsSDKError {
53
+ constructor(message?: string);
54
+ }
55
+ interface SecretsManagementOptions {
56
+ appId: string;
57
+ baseUrl?: string;
58
+ timeout?: number;
59
+ }
60
+ interface SecretConfig {
61
+ name: string;
62
+ provider: string;
63
+ api_key: string;
64
+ base_url?: string;
65
+ auth_header?: string;
66
+ auth_prefix?: string;
67
+ }
68
+ interface MaskedSecret {
69
+ id: string;
70
+ name: string;
71
+ provider: string;
72
+ api_key: string;
73
+ base_url?: string;
74
+ auth_header?: string;
75
+ auth_prefix?: string;
76
+ created?: string;
77
+ updated?: string;
78
+ }
79
+ interface SyncOptions {
80
+ deleteMissing?: boolean;
81
+ dryRun?: boolean;
82
+ }
83
+ interface SyncDiffItem {
84
+ name: string;
85
+ provider?: string;
86
+ changes?: string[];
87
+ }
88
+ interface SyncResult {
89
+ success: boolean;
90
+ diff: {
91
+ created: SyncDiffItem[];
92
+ updated: SyncDiffItem[];
93
+ deleted: SyncDiffItem[];
94
+ unchanged: SyncDiffItem[];
95
+ };
96
+ summary: {
97
+ created: number;
98
+ updated: number;
99
+ deleted: number;
100
+ unchanged: number;
101
+ };
102
+ }
103
+ interface CLICredentials {
104
+ access_token: string;
105
+ refresh_token: string;
106
+ expires_at: string;
107
+ user_id: string;
108
+ }
109
+
110
+ declare class SecretsSDK {
111
+ private appId;
112
+ private token;
113
+ private baseUrl;
114
+ private timeout;
115
+ private retryOn429;
116
+ private rateLimitInfo;
117
+ private zeroConfigMode;
118
+ /**
119
+ * Create a new SecretsSDK instance
120
+ *
121
+ * Zero-config mode (recommended for static sites):
122
+ * const sdk = new SecretsSDK();
123
+ * // Origin header is used for authentication
124
+ *
125
+ * Token mode (for backward compatibility):
126
+ * const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
127
+ */
128
+ constructor(options?: SecretsSDKOptions);
129
+ /**
130
+ * Get current rate limit information
131
+ */
132
+ getUsage(): RateLimitInfo | null;
133
+ /**
134
+ * Parse rate limit headers from response
135
+ */
136
+ private parseRateLimitHeaders;
137
+ /**
138
+ * Sleep utility for retry backoff
139
+ */
140
+ private sleep;
141
+ /**
142
+ * Make fetch request with timeout
143
+ */
144
+ private fetchWithTimeout;
145
+ /**
146
+ * Call an external API securely through the proxy
147
+ * @param keyName - Name of the API key to use
148
+ * @param endpoint - API endpoint path (e.g., '/v1/chat/completions')
149
+ * @param options - Request options (method, body, headers)
150
+ * @returns Promise with the API response
151
+ */
152
+ call<T = any>(keyName: string, endpoint: string, options?: Omit<ProxyRequest, 'keyName' | 'endpoint'>): Promise<T>;
153
+ /**
154
+ * Make a GET request
155
+ */
156
+ get<T = any>(keyName: string, endpoint: string, headers?: Record<string, string>): Promise<T>;
157
+ /**
158
+ * Make a POST request
159
+ */
160
+ post<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
161
+ /**
162
+ * Make a PUT request
163
+ */
164
+ put<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
165
+ /**
166
+ * Make a DELETE request
167
+ */
168
+ delete<T = any>(keyName: string, endpoint: string, headers?: Record<string, string>): Promise<T>;
169
+ /**
170
+ * Make a PATCH request
171
+ */
172
+ patch<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
173
+ /**
174
+ * Secrets Management API (zero-config mode)
175
+ * These methods allow programmatic CRUD operations on project secrets
176
+ * Requires appId to be set (can be set in constructor or via setAppId)
177
+ */
178
+ /**
179
+ * Set App ID for secrets management
180
+ * Required before calling secrets management methods
181
+ */
182
+ setAppId(appId: string): void;
183
+ /**
184
+ * Make authenticated request to secrets management API
185
+ */
186
+ private secretsRequest;
187
+ /**
188
+ * List all secrets for the project
189
+ * Returns secret metadata (no API keys)
190
+ */
191
+ listSecrets(): Promise<{
192
+ success: boolean;
193
+ secrets: Array<{
194
+ name: string;
195
+ provider: string;
196
+ base_url: string;
197
+ auth_header: string;
198
+ auth_prefix: string;
199
+ created: string;
200
+ updated: string;
201
+ }>;
202
+ count: number;
203
+ }>;
204
+ /**
205
+ * Create a new secret
206
+ */
207
+ createSecret(secret: {
208
+ name: string;
209
+ provider: string;
210
+ api_key: string;
211
+ base_url?: string;
212
+ auth_header?: string;
213
+ auth_prefix?: string;
214
+ }): Promise<{
215
+ success: boolean;
216
+ message: string;
217
+ secret: any;
218
+ }>;
219
+ /**
220
+ * Update an existing secret
221
+ */
222
+ updateSecret(update: {
223
+ name: string;
224
+ api_key?: string;
225
+ provider?: string;
226
+ base_url?: string;
227
+ auth_header?: string;
228
+ auth_prefix?: string;
229
+ }): Promise<{
230
+ success: boolean;
231
+ message: string;
232
+ secret: any;
233
+ }>;
234
+ /**
235
+ * Delete a secret
236
+ */
237
+ deleteSecret(name: string): Promise<{
238
+ success: boolean;
239
+ message: string;
240
+ }>;
241
+ /**
242
+ * Sync secrets from .env file format
243
+ * Bulk upload/update secrets
244
+ */
245
+ syncEnv(secrets: Record<string, string>, provider?: string): Promise<{
246
+ success: boolean;
247
+ message: string;
248
+ results: {
249
+ created: number;
250
+ updated: number;
251
+ failed: number;
252
+ details: {
253
+ created: string[];
254
+ updated: string[];
255
+ failed: Array<{
256
+ name: string;
257
+ error: string;
258
+ }>;
259
+ };
260
+ };
261
+ }>;
262
+ }
263
+
264
+ /**
265
+ * SecretsManagement class for CLI authentication and secrets management
266
+ */
267
+ declare class SecretsManagement {
268
+ private appId;
269
+ private baseUrl;
270
+ private timeout;
271
+ constructor(options: SecretsManagementOptions);
272
+ /**
273
+ * Open a URL in the default browser
274
+ */
275
+ private openBrowser;
276
+ /**
277
+ * Sleep for specified milliseconds
278
+ */
279
+ private sleep;
280
+ /**
281
+ * Browser OAuth login flow
282
+ * Opens browser for user to authorize, then stores credentials locally
283
+ */
284
+ login(): Promise<{
285
+ success: boolean;
286
+ user: string;
287
+ }>;
288
+ /**
289
+ * Check if user is authenticated
290
+ */
291
+ isAuthenticated(): Promise<boolean>;
292
+ /**
293
+ * Logout - clear stored credentials
294
+ */
295
+ logout(): void;
296
+ /**
297
+ * Get access token from stored credentials
298
+ * Throws error if not authenticated
299
+ */
300
+ private getAccessToken;
301
+ /**
302
+ * Make authenticated request
303
+ */
304
+ private request;
305
+ /**
306
+ * List all secrets (with masked API keys)
307
+ */
308
+ list(): Promise<MaskedSecret[]>;
309
+ /**
310
+ * Sync secrets from config
311
+ */
312
+ sync(secrets: SecretConfig[], options?: SyncOptions): Promise<SyncResult>;
313
+ /**
314
+ * Import secrets from a config object (bulk import)
315
+ * Also creates SDK token if origins are provided
316
+ *
317
+ * @param config - The secrets configuration
318
+ * @returns Import results including created/updated counts and optional SDK token
319
+ */
320
+ importSecrets(config: {
321
+ version?: string;
322
+ origins?: string[];
323
+ secrets: SecretConfig[];
324
+ }): Promise<{
325
+ success: boolean;
326
+ results: {
327
+ created: number;
328
+ updated: number;
329
+ failed: number;
330
+ details: {
331
+ created: string[];
332
+ updated: string[];
333
+ failed: {
334
+ name: string;
335
+ error: string;
336
+ }[];
337
+ };
338
+ };
339
+ sdkToken?: {
340
+ token: string;
341
+ message: string;
342
+ };
343
+ }>;
344
+ /**
345
+ * Import secrets from a JSON file path
346
+ * Resolves environment variables in the config
347
+ */
348
+ importFromFile(configPath: string): Promise<{
349
+ success: boolean;
350
+ results: {
351
+ created: number;
352
+ updated: number;
353
+ failed: number;
354
+ };
355
+ sdkToken?: {
356
+ token: string;
357
+ message: string;
358
+ };
359
+ }>;
360
+ }
361
+
362
+ /**
363
+ * Resolve environment variable references in a string
364
+ * Supports ${VAR} and $VAR syntax
365
+ */
366
+ declare function resolveEnvString(value: string): string;
367
+ /**
368
+ * Resolve environment variables in a single secret config
369
+ */
370
+ declare function resolveEnvInSecret(secret: SecretConfig): SecretConfig;
371
+ /**
372
+ * Resolve environment variables in an array of secrets
373
+ */
374
+ declare function resolveEnvInSecrets(secrets: SecretConfig[]): SecretConfig[];
375
+ /**
376
+ * Load and parse a secrets config file
377
+ * Resolves environment variables in the config
378
+ */
379
+ declare function loadSecretsConfig(configPath: string): {
380
+ version: string;
381
+ project: string;
382
+ secrets: SecretConfig[];
383
+ };
384
+
385
+ export { type CLICredentials, InvalidTokenError, type MaskedSecret, OriginMismatchError, type ProxyRequest, type ProxyResponse, RateLimitError, type RateLimitInfo, type SecretConfig, SecretsManagement, type SecretsManagementOptions, SecretsSDK, SecretsSDKError, type SecretsSDKOptions, type SyncOptions, type SyncResult, loadSecretsConfig, resolveEnvInSecret, resolveEnvInSecrets, resolveEnvString };
@@ -0,0 +1,7 @@
1
+ "use strict";var SecretsSDK=(()=>{var U=Object.create;var b=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var l=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var K=(r,e)=>()=>(r&&(e=r(r=0)),e);var O=(r,e)=>{for(var t in e)b(r,t,{get:e[t],enumerable:!0})},D=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of F(e))!J.call(r,n)&&n!==t&&b(r,n,{get:()=>e[n],enumerable:!(s=N(e,n))||s.enumerable});return r};var w=(r,e,t)=>(t=r!=null?U(z(r)):{},D(e||!r||!r.__esModule?b(t,"default",{value:r,enumerable:!0}):t,r)),H=r=>D(b({},"__esModule",{value:!0}),r);var j={};O(j,{loadSecretsConfig:()=>I,resolveEnvInSecret:()=>x,resolveEnvInSecrets:()=>k,resolveEnvString:()=>d});function d(r){return r.replace(/\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g,(e,t,s)=>{let n=t||s,a=process.env[n];if(a===void 0)throw new Error(`Environment variable ${n} is not defined`);return a})}function x(r){let e={name:d(r.name),provider:d(r.provider),api_key:d(r.api_key)};return r.base_url&&(e.base_url=d(r.base_url)),r.auth_header&&(e.auth_header=d(r.auth_header)),r.auth_prefix&&(e.auth_prefix=d(r.auth_prefix)),e}function k(r){return r.map(x)}function I(r){let e=l("fs"),s=l("path").resolve(r);if(!e.existsSync(s))throw new Error(`Config file not found: ${s}`);let n=e.readFileSync(s,"utf-8"),a=JSON.parse(n);if(!a.version)throw new Error('Config file missing "version" field');if(!a.project)throw new Error('Config file missing "project" field');if(!Array.isArray(a.secrets))throw new Error('Config file missing "secrets" array');let i=k(a.secrets);return{version:a.version,project:a.project,secrets:i}}var P=K(()=>{"use strict"});var G={};O(G,{InvalidTokenError:()=>h,OriginMismatchError:()=>m,RateLimitError:()=>f,SecretsManagement:()=>_,SecretsSDK:()=>S,SecretsSDKError:()=>o,loadSecretsConfig:()=>I,resolveEnvInSecret:()=>x,resolveEnvInSecrets:()=>k,resolveEnvString:()=>d});var o=class extends Error{constructor(t,s,n){super(t);this.status=s;this.response=n;this.name="SecretsSDKError"}},m=class extends o{constructor(e="Origin not allowed for this token"){super(e,403),this.name="OriginMismatchError"}},f=class extends o{constructor(e="Rate limit exceeded",t=60,s=0,n=100){super(e,429),this.name="RateLimitError",this.retryAfter=t,this.remaining=s,this.limit=n}},h=class extends o{constructor(e="Invalid or expired token"){super(e,401),this.name="InvalidTokenError"}};var S=class{constructor(e={}){this.rateLimitInfo=null;this.appId=e.appId||null,this.token=e.token||e.sessionToken||null,this.baseUrl=e.baseUrl||"https://ctklearn.carsontkempf.workers.dev",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"),s=e.get("X-RateLimit-Remaining"),n=e.get("X-RateLimit-Reset");t&&s&&n&&(this.rateLimitInfo={limit:parseInt(t),remaining:parseInt(s),reset:parseInt(n)})}sleep(e){return new Promise(t=>setTimeout(t,e))}async fetchWithTimeout(e,t,s){let n=new AbortController,a=setTimeout(()=>n.abort(),s);try{return await fetch(e,{...t,signal:n.signal})}finally{clearTimeout(a)}}async call(e,t,s={}){let n=0,a=this.retryOn429?3:0;for(;;)try{let i=this.zeroConfigMode?`${this.baseUrl}/api/sdk/proxy`:`${this.baseUrl}/api/sdk/${this.appId}/proxy`,p={"Content-Type":"application/json"};!this.zeroConfigMode&&this.token&&(p.Authorization=`Bearer ${this.token}`);let u=await this.fetchWithTimeout(i,{method:"POST",headers:p,body:JSON.stringify({keyName:e,endpoint:t,method:s.method||"GET",body:s.body,headers:s.headers})},this.timeout);this.parseRateLimitHeaders(u.headers);let g=await u.json();if(!u.ok){let y=g.data?.message||g?.message||`Request failed with status ${u.status}`;if(u.status===403&&y.includes("Origin"))throw new m(y);if(u.status===401)throw new h(y);if(u.status===429){let $=this.rateLimitInfo?this.rateLimitInfo.reset-Math.floor(Date.now()/1e3):60,q=new f(y,$,this.rateLimitInfo?.remaining||0,this.rateLimitInfo?.limit||100);if(this.retryOn429&&n<a){n++;let M=Math.min(1e3*Math.pow(2,n-1),8e3);await this.sleep(M);continue}throw q}throw new o(y,u.status,g)}return g.data}catch(i){throw i instanceof o?i:i instanceof Error&&i.name==="AbortError"?new o("Request timeout",408):new o(i instanceof Error?i.message:"Unknown error occurred",500)}}async get(e,t,s){return this.call(e,t,{method:"GET",headers:s})}async post(e,t,s,n){return this.call(e,t,{method:"POST",body:s,headers:n})}async put(e,t,s,n){return this.call(e,t,{method:"PUT",body:s,headers:n})}async delete(e,t,s){return this.call(e,t,{method:"DELETE",headers:s})}async patch(e,t,s,n){return this.call(e,t,{method:"PATCH",body:s,headers:n})}setAppId(e){this.appId=e}async secretsRequest(e,t){if(!this.appId)throw new o("App ID required for secrets management. Call setAppId() first.",400);let s=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(!s.ok){let n=await s.json().catch(()=>({}));throw new o(n.message||`Secrets management failed: ${s.statusText}`,s.status,n)}return s.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})}};var c=w(l("fs")),v=w(l("path")),T=w(l("os"));function R(){let r=T.homedir(),e=v.join(r,".learn");return v.join(e,"credentials.json")}function B(){let r=T.homedir(),e=v.join(r,".learn");c.existsSync(e)||c.mkdirSync(e,{recursive:!0,mode:448})}function C(){try{let r=R();if(!c.existsSync(r))return null;let e=c.readFileSync(r,"utf-8"),t=JSON.parse(e);return!t.access_token||!t.refresh_token||!t.expires_at||!t.user_id?null:t}catch{return null}}function E(r){B();let e=R(),t=JSON.stringify(r,null,2);c.writeFileSync(e,t,{mode:384})}function A(){try{let r=R();c.existsSync(r)&&c.unlinkSync(r)}catch{}}function L(){let r=C();return r?new Date(r.expires_at)>new Date:!1}var _=class{constructor(e){this.appId=e.appId,this.baseUrl=e.baseUrl||"https://ctklearn.carsontkempf.workers.dev",this.timeout=e.timeout||3e4}async openBrowser(e){let{exec:t}=await import("child_process"),{promisify:s}=await import("util"),n=s(t),a=process.platform;try{a==="darwin"?await n(`open "${e}"`):a==="win32"?await n(`start "${e}"`):await n(`xdg-open "${e}"`)}catch(i){throw console.error("Failed to open browser:",i),new o("Failed to open browser",500)}}sleep(e){return new Promise(t=>setTimeout(t,e))}async login(){try{let e=await fetch(`${this.baseUrl}/api/auth/cli/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!e.ok)throw new o("Failed to generate device code",e.status);let t=await e.json();console.log(`
2
+ To authorize this application, visit:`),console.log(` ${t.verification_uri}`),console.log(`
3
+ And enter the code:`),console.log(` ${t.user_code}`),console.log(`
4
+ Opening browser...
5
+ `),await this.openBrowser(t.verification_uri_complete);let s=t.interval*1e3,n=Math.ceil(t.expires_in/t.interval),a=0;for(;a<n;){await this.sleep(s),a++;let i=await fetch(`${this.baseUrl}/api/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:t.device_code})});if(i.status===202){process.stdout.write(".");continue}if(!i.ok){let g=await i.json().catch(()=>({}));throw new o(g.message||"Failed to exchange device code",i.status)}let p=await i.json(),u=new Date(Date.now()+p.expires_in*1e3);return E({access_token:p.access_token,refresh_token:p.refresh_token,expires_at:u.toISOString(),user_id:p.user_id}),console.log(`
6
+
7
+ Authentication successful!`),{success:!0,user:p.user_id}}throw new o("Authorization timeout",408)}catch(e){throw e instanceof o?e:new o(e.message||"Login failed",e.status||500)}}async isAuthenticated(){return L()}logout(){A(),console.log("Logged out successfully")}getAccessToken(){let e=C();if(!e)throw new o("Not authenticated. Run login() first.",401);if(new Date(e.expires_at)<=new Date)throw new o("Token expired. Run login() again.",401);return e.access_token}async request(e,t,s){let n=this.getAccessToken(),a={method:e,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`}};s&&(a.body=JSON.stringify(s));let i=await fetch(`${this.baseUrl}${t}`,a);if(!i.ok){let p=await i.json().catch(()=>({}));throw new o(p.message||`Request failed: ${i.statusText}`,i.status,p)}return i.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 Promise.resolve().then(()=>(P(),j)),s=t(e);return this.importSecrets(s)}};P();return H(G);})();
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import{a as P,b as C,c as O,d as D}from"./chunk-AS6G7JYX.mjs";var i=class extends Error{constructor(t,r,s){super(t);this.status=r;this.response=s;this.name="SecretsSDKError"}},m=class extends i{constructor(e="Origin not allowed for this token"){super(e,403),this.name="OriginMismatchError"}},g=class extends i{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}},h=class extends i{constructor(e="Invalid or expired token"){super(e,401),this.name="InvalidTokenError"}};var y=class{constructor(e={}){this.rateLimitInfo=null;this.appId=e.appId||null,this.token=e.token||e.sessionToken||null,this.baseUrl=e.baseUrl||"https://ctklearn.carsontkempf.workers.dev",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,a=setTimeout(()=>s.abort(),r);try{return await fetch(e,{...t,signal:s.signal})}finally{clearTimeout(a)}}async call(e,t,r={}){let s=0,a=this.retryOn429?3:0;for(;;)try{let n=this.zeroConfigMode?`${this.baseUrl}/api/sdk/proxy`:`${this.baseUrl}/api/sdk/${this.appId}/proxy`,p={"Content-Type":"application/json"};!this.zeroConfigMode&&this.token&&(p.Authorization=`Bearer ${this.token}`);let u=await this.fetchWithTimeout(n,{method:"POST",headers:p,body:JSON.stringify({keyName:e,endpoint:t,method:r.method||"GET",body:r.body,headers:r.headers})},this.timeout);this.parseRateLimitHeaders(u.headers);let d=await u.json();if(!u.ok){let l=d.data?.message||d?.message||`Request failed with status ${u.status}`;if(u.status===403&&l.includes("Origin"))throw new m(l);if(u.status===401)throw new h(l);if(u.status===429){let R=this.rateLimitInfo?this.rateLimitInfo.reset-Math.floor(Date.now()/1e3):60,_=new g(l,R,this.rateLimitInfo?.remaining||0,this.rateLimitInfo?.limit||100);if(this.retryOn429&&s<a){s++;let I=Math.min(1e3*Math.pow(2,s-1),8e3);await this.sleep(I);continue}throw _}throw new i(l,u.status,d)}return d.data}catch(n){throw n instanceof i?n:n instanceof Error&&n.name==="AbortError"?new i("Request timeout",408):new i(n instanceof Error?n.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 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 s=await r.json().catch(()=>({}));throw new i(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})}};import*as c from"fs";import*as f from"path";import*as w from"os";function b(){let o=w.homedir(),e=f.join(o,".learn");return f.join(e,"credentials.json")}function L(){let o=w.homedir(),e=f.join(o,".learn");c.existsSync(e)||c.mkdirSync(e,{recursive:!0,mode:448})}function x(){try{let o=b();if(!c.existsSync(o))return null;let e=c.readFileSync(o,"utf-8"),t=JSON.parse(e);return!t.access_token||!t.refresh_token||!t.expires_at||!t.user_id?null:t}catch{return null}}function k(o){L();let e=b(),t=JSON.stringify(o,null,2);c.writeFileSync(e,t,{mode:384})}function v(){try{let o=b();c.existsSync(o)&&c.unlinkSync(o)}catch{}}function T(){let o=x();return o?new Date(o.expires_at)>new Date:!1}var S=class{constructor(e){this.appId=e.appId,this.baseUrl=e.baseUrl||"https://ctklearn.carsontkempf.workers.dev",this.timeout=e.timeout||3e4}async openBrowser(e){let{exec:t}=await import("child_process"),{promisify:r}=await import("util"),s=r(t),a=process.platform;try{a==="darwin"?await s(`open "${e}"`):a==="win32"?await s(`start "${e}"`):await s(`xdg-open "${e}"`)}catch(n){throw console.error("Failed to open browser:",n),new i("Failed to open browser",500)}}sleep(e){return new Promise(t=>setTimeout(t,e))}async login(){try{let e=await fetch(`${this.baseUrl}/api/auth/cli/device`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!e.ok)throw new i("Failed to generate device code",e.status);let t=await e.json();console.log(`
2
+ To authorize this application, visit:`),console.log(` ${t.verification_uri}`),console.log(`
3
+ And enter the code:`),console.log(` ${t.user_code}`),console.log(`
4
+ Opening browser...
5
+ `),await this.openBrowser(t.verification_uri_complete);let r=t.interval*1e3,s=Math.ceil(t.expires_in/t.interval),a=0;for(;a<s;){await this.sleep(r),a++;let n=await fetch(`${this.baseUrl}/api/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:t.device_code})});if(n.status===202){process.stdout.write(".");continue}if(!n.ok){let d=await n.json().catch(()=>({}));throw new i(d.message||"Failed to exchange device code",n.status)}let p=await n.json(),u=new Date(Date.now()+p.expires_in*1e3);return k({access_token:p.access_token,refresh_token:p.refresh_token,expires_at:u.toISOString(),user_id:p.user_id}),console.log(`
6
+
7
+ Authentication successful!`),{success:!0,user:p.user_id}}throw new i("Authorization timeout",408)}catch(e){throw e instanceof i?e:new i(e.message||"Login failed",e.status||500)}}async isAuthenticated(){return T()}logout(){v(),console.log("Logged out successfully")}getAccessToken(){let e=x();if(!e)throw new i("Not authenticated. Run login() first.",401);if(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 s=this.getAccessToken(),a={method:e,headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`}};r&&(a.body=JSON.stringify(r));let n=await fetch(`${this.baseUrl}${t}`,a);if(!n.ok){let p=await n.json().catch(()=>({}));throw new i(p.message||`Request failed: ${n.statusText}`,n.status,p)}return n.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)}};export{h as InvalidTokenError,m as OriginMismatchError,g as RateLimitError,S as SecretsManagement,y as SecretsSDK,i as SecretsSDKError,D as loadSecretsConfig,C as resolveEnvInSecret,O as resolveEnvInSecrets,P as resolveEnvString};
package/package.json CHANGED
@@ -1,27 +1,45 @@
1
1
  {
2
2
  "name": "learn-secrets-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Secure API proxy SDK for static sites with domain-scoped tokens",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "learn-secrets": "./bin/learn-secrets.js"
10
+ },
8
11
  "files": [
9
- "dist"
12
+ "dist",
13
+ "bin",
14
+ "secrets.template.json"
10
15
  ],
11
- "scripts": {
12
- "build": "tsc && tsc --project tsconfig.esm.json",
13
- "prepublishOnly": "npm run build"
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/carsontkempf/learn.git",
19
+ "directory": "packages/secrets-sdk"
14
20
  },
21
+ "homepage": "https://github.com/carsontkempf/learn/tree/main/packages/secrets-sdk",
22
+ "author": "Carson Kempf",
15
23
  "keywords": [
16
24
  "api",
17
25
  "proxy",
18
26
  "secrets",
19
27
  "static-sites",
20
- "security"
28
+ "security",
29
+ "cli",
30
+ "secrets-management"
21
31
  ],
22
- "author": "",
23
32
  "license": "MIT",
33
+ "dependencies": {
34
+ "commander": "^12.0.0"
35
+ },
24
36
  "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "tsup": "^8.5.1",
25
39
  "typescript": "^5.0.0"
40
+ },
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",
43
+ "prepublishOnly": "npm run build"
26
44
  }
27
45
  }
@@ -0,0 +1,42 @@
1
+ {
2
+ "version": "1.0",
3
+ "project": "your-app-id",
4
+ "origins": [
5
+ "localhost",
6
+ "yourdomain.com"
7
+ ],
8
+ "secrets": [
9
+ {
10
+ "name": "test-echo",
11
+ "provider": "custom",
12
+ "api_key": "test",
13
+ "base_url": "https://httpbin.org",
14
+ "auth_header": "Authorization",
15
+ "auth_prefix": "Bearer "
16
+ },
17
+ {
18
+ "name": "openai",
19
+ "provider": "openai",
20
+ "api_key": "${OPENAI_API_KEY}",
21
+ "base_url": "https://api.openai.com",
22
+ "auth_header": "Authorization",
23
+ "auth_prefix": "Bearer "
24
+ },
25
+ {
26
+ "name": "anthropic",
27
+ "provider": "anthropic",
28
+ "api_key": "${ANTHROPIC_API_KEY}",
29
+ "base_url": "https://api.anthropic.com",
30
+ "auth_header": "x-api-key",
31
+ "auth_prefix": ""
32
+ },
33
+ {
34
+ "name": "stripe",
35
+ "provider": "stripe",
36
+ "api_key": "${STRIPE_SECRET_KEY}",
37
+ "base_url": "https://api.stripe.com",
38
+ "auth_header": "Authorization",
39
+ "auth_prefix": "Bearer "
40
+ }
41
+ ]
42
+ }
package/dist/client.d.ts DELETED
@@ -1,65 +0,0 @@
1
- import type { SecretsSDKOptions, ProxyRequest, RateLimitInfo } from './types';
2
- export declare class SecretsSDK {
3
- private appId;
4
- private token;
5
- private baseUrl;
6
- private timeout;
7
- private retryOn429;
8
- private rateLimitInfo;
9
- private zeroConfigMode;
10
- /**
11
- * Create a new SecretsSDK instance
12
- *
13
- * Zero-config mode (recommended for static sites):
14
- * const sdk = new SecretsSDK();
15
- * // Origin header is used for authentication
16
- *
17
- * Token mode (for backward compatibility):
18
- * const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
19
- */
20
- constructor(options?: SecretsSDKOptions);
21
- /**
22
- * Get current rate limit information
23
- */
24
- getUsage(): RateLimitInfo | null;
25
- /**
26
- * Parse rate limit headers from response
27
- */
28
- private parseRateLimitHeaders;
29
- /**
30
- * Sleep utility for retry backoff
31
- */
32
- private sleep;
33
- /**
34
- * Make fetch request with timeout
35
- */
36
- private fetchWithTimeout;
37
- /**
38
- * Call an external API securely through the proxy
39
- * @param keyName - Name of the API key to use
40
- * @param endpoint - API endpoint path (e.g., '/v1/chat/completions')
41
- * @param options - Request options (method, body, headers)
42
- * @returns Promise with the API response
43
- */
44
- call<T = any>(keyName: string, endpoint: string, options?: Omit<ProxyRequest, 'keyName' | 'endpoint'>): Promise<T>;
45
- /**
46
- * Make a GET request
47
- */
48
- get<T = any>(keyName: string, endpoint: string, headers?: Record<string, string>): Promise<T>;
49
- /**
50
- * Make a POST request
51
- */
52
- post<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
53
- /**
54
- * Make a PUT request
55
- */
56
- put<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
57
- /**
58
- * Make a DELETE request
59
- */
60
- delete<T = any>(keyName: string, endpoint: string, headers?: Record<string, string>): Promise<T>;
61
- /**
62
- * Make a PATCH request
63
- */
64
- patch<T = any>(keyName: string, endpoint: string, body: any, headers?: Record<string, string>): Promise<T>;
65
- }