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.
@@ -0,0 +1,838 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-Y6FXYEAI.mjs";
3
+
4
+ // src/cli/index.ts
5
+ import { Command } from "commander";
6
+
7
+ // src/types.ts
8
+ var SecretsSDKError = class extends Error {
9
+ constructor(message, status, response) {
10
+ super(message);
11
+ this.status = status;
12
+ this.response = response;
13
+ this.name = "SecretsSDKError";
14
+ }
15
+ };
16
+
17
+ // src/credentials.ts
18
+ import * as fs from "fs";
19
+ import * as path from "path";
20
+ import * as os from "os";
21
+ function getCredentialsPath() {
22
+ const homeDir = os.homedir();
23
+ const learnDir = path.join(homeDir, ".learn");
24
+ return path.join(learnDir, "credentials.json");
25
+ }
26
+ function ensureLearnDir() {
27
+ const homeDir = os.homedir();
28
+ const learnDir = path.join(homeDir, ".learn");
29
+ if (!fs.existsSync(learnDir)) {
30
+ fs.mkdirSync(learnDir, { recursive: true, mode: 448 });
31
+ }
32
+ }
33
+ function readCredentials() {
34
+ try {
35
+ const credPath = getCredentialsPath();
36
+ if (!fs.existsSync(credPath)) {
37
+ return null;
38
+ }
39
+ const data = fs.readFileSync(credPath, "utf-8");
40
+ const creds = JSON.parse(data);
41
+ if (!creds.access_token || !creds.refresh_token || !creds.expires_at || !creds.user_id) {
42
+ return null;
43
+ }
44
+ return creds;
45
+ } catch (err) {
46
+ return null;
47
+ }
48
+ }
49
+ function writeCredentials(creds) {
50
+ ensureLearnDir();
51
+ const credPath = getCredentialsPath();
52
+ const data = JSON.stringify(creds, null, 2);
53
+ fs.writeFileSync(credPath, data, { mode: 384 });
54
+ }
55
+ function deleteCredentials() {
56
+ try {
57
+ const credPath = getCredentialsPath();
58
+ if (fs.existsSync(credPath)) {
59
+ fs.unlinkSync(credPath);
60
+ }
61
+ } catch (err) {
62
+ }
63
+ }
64
+ function hasValidCredentials() {
65
+ const creds = readCredentials();
66
+ if (!creds) {
67
+ return false;
68
+ }
69
+ const expiresAt = new Date(creds.expires_at);
70
+ const now = /* @__PURE__ */ new Date();
71
+ return expiresAt > now;
72
+ }
73
+
74
+ // src/management.ts
75
+ var SecretsManagement = class {
76
+ constructor(options) {
77
+ this.appId = options.appId;
78
+ this.baseUrl = options.baseUrl || "https://ctklearn.carsontkempf.workers.dev";
79
+ this.timeout = options.timeout || 3e4;
80
+ }
81
+ /**
82
+ * Open a URL in the default browser
83
+ */
84
+ async openBrowser(url) {
85
+ const { exec } = await import("child_process");
86
+ const { promisify } = await import("util");
87
+ const execAsync = promisify(exec);
88
+ const platform = process.platform;
89
+ try {
90
+ if (platform === "darwin") {
91
+ await execAsync(`open "${url}"`);
92
+ } else if (platform === "win32") {
93
+ await execAsync(`start "${url}"`);
94
+ } else {
95
+ await execAsync(`xdg-open "${url}"`);
96
+ }
97
+ } catch (err) {
98
+ console.error("Failed to open browser:", err);
99
+ throw new SecretsSDKError("Failed to open browser", 500);
100
+ }
101
+ }
102
+ /**
103
+ * Sleep for specified milliseconds
104
+ */
105
+ sleep(ms) {
106
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
107
+ }
108
+ /**
109
+ * Browser OAuth login flow
110
+ * Opens browser for user to authorize, then stores credentials locally
111
+ */
112
+ async login() {
113
+ try {
114
+ const deviceResponse = await fetch(`${this.baseUrl}/api/auth/cli/device`, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": "application/json"
118
+ }
119
+ });
120
+ if (!deviceResponse.ok) {
121
+ throw new SecretsSDKError(
122
+ "Failed to generate device code",
123
+ deviceResponse.status
124
+ );
125
+ }
126
+ const deviceData = await deviceResponse.json();
127
+ console.log("\nTo authorize this application, visit:");
128
+ console.log(` ${deviceData.verification_uri}`);
129
+ console.log("\nAnd enter the code:");
130
+ console.log(` ${deviceData.user_code}`);
131
+ console.log("\nOpening browser...\n");
132
+ await this.openBrowser(deviceData.verification_uri_complete);
133
+ const interval = deviceData.interval * 1e3;
134
+ const maxAttempts = Math.ceil(deviceData.expires_in / deviceData.interval);
135
+ let attempts = 0;
136
+ while (attempts < maxAttempts) {
137
+ await this.sleep(interval);
138
+ attempts++;
139
+ const tokenResponse = await fetch(`${this.baseUrl}/api/auth/cli/token`, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json"
143
+ },
144
+ body: JSON.stringify({
145
+ device_code: deviceData.device_code
146
+ })
147
+ });
148
+ if (tokenResponse.status === 202) {
149
+ process.stdout.write(".");
150
+ continue;
151
+ }
152
+ if (!tokenResponse.ok) {
153
+ const errorData = await tokenResponse.json().catch(() => ({}));
154
+ throw new SecretsSDKError(
155
+ errorData.message || "Failed to exchange device code",
156
+ tokenResponse.status
157
+ );
158
+ }
159
+ const tokenData = await tokenResponse.json();
160
+ const expiresAt = new Date(Date.now() + tokenData.expires_in * 1e3);
161
+ writeCredentials({
162
+ access_token: tokenData.access_token,
163
+ refresh_token: tokenData.refresh_token,
164
+ expires_at: expiresAt.toISOString(),
165
+ user_id: tokenData.user_id
166
+ });
167
+ console.log("\n\nAuthentication successful!");
168
+ return {
169
+ success: true,
170
+ user: tokenData.user_id
171
+ };
172
+ }
173
+ throw new SecretsSDKError("Authorization timeout", 408);
174
+ } catch (err) {
175
+ if (err instanceof SecretsSDKError) {
176
+ throw err;
177
+ }
178
+ throw new SecretsSDKError(
179
+ err.message || "Login failed",
180
+ err.status || 500
181
+ );
182
+ }
183
+ }
184
+ /**
185
+ * Check if user is authenticated
186
+ */
187
+ async isAuthenticated() {
188
+ return hasValidCredentials();
189
+ }
190
+ /**
191
+ * Logout - clear stored credentials
192
+ */
193
+ logout() {
194
+ deleteCredentials();
195
+ console.log("Logged out successfully");
196
+ }
197
+ /**
198
+ * Get access token from stored credentials
199
+ * Throws error if not authenticated
200
+ */
201
+ getAccessToken() {
202
+ const creds = readCredentials();
203
+ if (!creds) {
204
+ throw new SecretsSDKError("Not authenticated. Run login() first.", 401);
205
+ }
206
+ const expiresAt = new Date(creds.expires_at);
207
+ const now = /* @__PURE__ */ new Date();
208
+ if (expiresAt <= now) {
209
+ throw new SecretsSDKError("Token expired. Run login() again.", 401);
210
+ }
211
+ return creds.access_token;
212
+ }
213
+ /**
214
+ * Make authenticated request
215
+ */
216
+ async request(method, path2, body) {
217
+ const token = this.getAccessToken();
218
+ const options = {
219
+ method,
220
+ headers: {
221
+ "Content-Type": "application/json",
222
+ Authorization: `Bearer ${token}`
223
+ }
224
+ };
225
+ if (body) {
226
+ options.body = JSON.stringify(body);
227
+ }
228
+ const response = await fetch(`${this.baseUrl}${path2}`, options);
229
+ if (!response.ok) {
230
+ const errorData = await response.json().catch(() => ({}));
231
+ throw new SecretsSDKError(
232
+ errorData.message || `Request failed: ${response.statusText}`,
233
+ response.status,
234
+ errorData
235
+ );
236
+ }
237
+ return response.json();
238
+ }
239
+ /**
240
+ * List all secrets (with masked API keys)
241
+ */
242
+ async list() {
243
+ const response = await this.request(
244
+ "GET",
245
+ `/api/projects/${this.appId}/secrets`
246
+ );
247
+ return response.secrets;
248
+ }
249
+ /**
250
+ * Sync secrets from config
251
+ */
252
+ async sync(secrets, options) {
253
+ const response = await this.request(
254
+ "POST",
255
+ `/api/projects/${this.appId}/secrets/sync`,
256
+ {
257
+ secrets,
258
+ options: {
259
+ deleteMissing: options?.deleteMissing ?? false,
260
+ dryRun: options?.dryRun ?? false
261
+ }
262
+ }
263
+ );
264
+ return response;
265
+ }
266
+ /**
267
+ * Import secrets from a config object (bulk import)
268
+ * Also creates SDK token if origins are provided
269
+ *
270
+ * @param config - The secrets configuration
271
+ * @returns Import results including created/updated counts and optional SDK token
272
+ */
273
+ async importSecrets(config) {
274
+ return this.request(
275
+ "POST",
276
+ `/api/projects/${this.appId}/import-secrets`,
277
+ {
278
+ version: config.version || "1.0",
279
+ origins: config.origins,
280
+ secrets: config.secrets
281
+ }
282
+ );
283
+ }
284
+ /**
285
+ * Import secrets from a JSON file path
286
+ * Resolves environment variables in the config
287
+ */
288
+ async importFromFile(configPath) {
289
+ const { loadSecretsConfig } = await import("./env-resolver-4ASC2IMR.mjs");
290
+ const config = loadSecretsConfig(configPath);
291
+ return this.importSecrets(config);
292
+ }
293
+ };
294
+
295
+ // src/cli/commands/login.ts
296
+ async function loginCommand(options = {}) {
297
+ console.log("Authenticating with Learn Secrets...\n");
298
+ const mgmt = new SecretsManagement({
299
+ appId: "temp",
300
+ // Will be set after login
301
+ baseUrl: options.baseUrl
302
+ });
303
+ try {
304
+ const result = await mgmt.login();
305
+ console.log(`
306
+ Logged in as: ${result.user}`);
307
+ console.log("Credentials saved to ~/.learn/credentials.json");
308
+ console.log("\nYou can now run: learn-secrets init --origins yourdomain.com");
309
+ } catch (error) {
310
+ console.error("\nLogin failed:", error.message);
311
+ process.exit(1);
312
+ }
313
+ }
314
+
315
+ // src/cli/commands/init.ts
316
+ import fs2 from "fs";
317
+
318
+ // src/cli/utils/env-parser.ts
319
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
320
+ import { resolve } from "path";
321
+ function parseEnvFile(filePath) {
322
+ const resolvedPath = resolve(filePath);
323
+ if (!existsSync2(resolvedPath)) {
324
+ throw new Error(`File not found: ${resolvedPath}`);
325
+ }
326
+ const content = readFileSync2(resolvedPath, "utf-8");
327
+ const lines = content.split("\n");
328
+ const variables = [];
329
+ for (const line of lines) {
330
+ const trimmed = line.trim();
331
+ if (!trimmed || trimmed.startsWith("#")) {
332
+ continue;
333
+ }
334
+ const match = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(['"]?)(.+?)\2$/i);
335
+ if (match) {
336
+ const key = match[1];
337
+ const value = match[3];
338
+ variables.push({ key, value });
339
+ }
340
+ }
341
+ return variables;
342
+ }
343
+
344
+ // src/cli/utils/key-detector.ts
345
+ var PROVIDER_PATTERNS = [
346
+ {
347
+ pattern: /^OPENAI_API_KEY$/i,
348
+ provider: "openai",
349
+ baseUrl: "https://api.openai.com",
350
+ authHeader: "Authorization",
351
+ authPrefix: "Bearer "
352
+ },
353
+ {
354
+ pattern: /^ANTHROPIC_API_KEY$/i,
355
+ provider: "anthropic",
356
+ baseUrl: "https://api.anthropic.com",
357
+ authHeader: "x-api-key",
358
+ authPrefix: ""
359
+ },
360
+ {
361
+ pattern: /^STRIPE_(SECRET_)?KEY$/i,
362
+ provider: "stripe",
363
+ baseUrl: "https://api.stripe.com",
364
+ authHeader: "Authorization",
365
+ authPrefix: "Bearer "
366
+ },
367
+ {
368
+ pattern: /^GITHUB_TOKEN$/i,
369
+ provider: "github",
370
+ baseUrl: "https://api.github.com",
371
+ authHeader: "Authorization",
372
+ authPrefix: "Bearer "
373
+ },
374
+ {
375
+ pattern: /^GOOGLE_API_KEY$/i,
376
+ provider: "google",
377
+ baseUrl: "https://www.googleapis.com",
378
+ authHeader: "Authorization",
379
+ authPrefix: "Bearer "
380
+ }
381
+ ];
382
+ function isLikelyApiKey(key, value) {
383
+ const keyPatterns = [
384
+ /_API_KEY$/i,
385
+ /_SECRET$/i,
386
+ /_TOKEN$/i,
387
+ /^API_KEY_/i,
388
+ /^SECRET_/i,
389
+ /^TOKEN_/i
390
+ ];
391
+ for (const pattern of keyPatterns) {
392
+ if (pattern.test(key)) {
393
+ return true;
394
+ }
395
+ }
396
+ const valuePatterns = [
397
+ /^sk-[a-zA-Z0-9_-]+$/,
398
+ // OpenAI, Stripe
399
+ /^sk-ant-[a-zA-Z0-9_-]+$/,
400
+ // Anthropic
401
+ /^ghp_[a-zA-Z0-9]+$/,
402
+ // GitHub
403
+ /^ya29\.[a-zA-Z0-9_-]+$/,
404
+ // Google OAuth
405
+ /^[a-f0-9]{32}$/,
406
+ // 32-char hex
407
+ /^[a-f0-9]{40}$/,
408
+ // 40-char hex (like GitHub)
409
+ /^[a-zA-Z0-9_-]{40,}$/
410
+ // Long random string
411
+ ];
412
+ for (const pattern of valuePatterns) {
413
+ if (pattern.test(value)) {
414
+ return true;
415
+ }
416
+ }
417
+ return false;
418
+ }
419
+ function detectProvider(key) {
420
+ for (const pattern of PROVIDER_PATTERNS) {
421
+ if (pattern.pattern.test(key)) {
422
+ return pattern;
423
+ }
424
+ }
425
+ return null;
426
+ }
427
+ function envKeyToSecretName(key) {
428
+ return key.toLowerCase().replace(/_api_key$/i, "").replace(/_secret$/i, "").replace(/_token$/i, "").replace(/_key$/i, "").replace(/_/g, "-");
429
+ }
430
+ function detectApiKeys(envVars) {
431
+ const secrets = [];
432
+ for (const { key, value } of envVars) {
433
+ if (!isLikelyApiKey(key, value)) {
434
+ continue;
435
+ }
436
+ const providerInfo = detectProvider(key);
437
+ const secretName = envKeyToSecretName(key);
438
+ if (providerInfo) {
439
+ secrets.push({
440
+ name: secretName,
441
+ provider: providerInfo.provider,
442
+ api_key: value,
443
+ base_url: providerInfo.baseUrl,
444
+ auth_header: providerInfo.authHeader,
445
+ auth_prefix: providerInfo.authPrefix
446
+ });
447
+ } else {
448
+ secrets.push({
449
+ name: secretName,
450
+ provider: "custom",
451
+ api_key: value,
452
+ base_url: "",
453
+ // User must configure
454
+ auth_header: "Authorization",
455
+ auth_prefix: "Bearer "
456
+ });
457
+ }
458
+ }
459
+ return secrets;
460
+ }
461
+ function summarizeDetectedSecrets(secrets) {
462
+ if (secrets.length === 0) {
463
+ return "No API keys detected in .env file";
464
+ }
465
+ const lines = ["Detected API keys:"];
466
+ for (const secret of secrets) {
467
+ const masked = secret.api_key.substring(0, 8) + "...";
468
+ lines.push(` - ${secret.name} (${secret.provider}): ${masked}`);
469
+ }
470
+ return lines.join("\n");
471
+ }
472
+
473
+ // src/cli/commands/init.ts
474
+ async function initCommand(options) {
475
+ if (!hasValidCredentials()) {
476
+ console.error('Error: Not authenticated. Run "learn-secrets login" first.');
477
+ process.exit(1);
478
+ }
479
+ if (!options.origins) {
480
+ console.error("Error: --origins is required");
481
+ console.error("Example: learn-secrets init --origins mysite.com,localhost");
482
+ process.exit(1);
483
+ }
484
+ if (!options.project) {
485
+ console.error("Error: --project is required");
486
+ console.error("Example: learn-secrets init --project my-app-id --origins mysite.com");
487
+ process.exit(1);
488
+ }
489
+ const origins = options.origins.split(",").map((o) => o.trim()).filter(Boolean);
490
+ if (origins.length === 0) {
491
+ console.error("Error: No valid origins provided");
492
+ process.exit(1);
493
+ }
494
+ console.log(`
495
+ Onboarding site for project: ${options.project}`);
496
+ console.log(`Origins: ${origins.join(", ")}
497
+ `);
498
+ const envFile = options.env || ".env";
499
+ let secrets;
500
+ try {
501
+ console.log(`Reading ${envFile}...`);
502
+ const envVars = parseEnvFile(envFile);
503
+ secrets = detectApiKeys(envVars);
504
+ if (secrets.length === 0) {
505
+ console.log("\nNo API keys detected in .env file.");
506
+ console.log("Make sure your .env contains variables like:");
507
+ console.log(" OPENAI_API_KEY=sk-...");
508
+ console.log(" ANTHROPIC_API_KEY=sk-ant-...");
509
+ process.exit(0);
510
+ }
511
+ console.log("\n" + summarizeDetectedSecrets(secrets));
512
+ console.log("");
513
+ } catch (error) {
514
+ console.error(`Error reading ${envFile}:`, error.message);
515
+ process.exit(1);
516
+ }
517
+ if (!options.yes) {
518
+ const readline = await import("readline");
519
+ const rl = readline.createInterface({
520
+ input: process.stdin,
521
+ output: process.stdout
522
+ });
523
+ const answer = await new Promise((resolve2) => {
524
+ rl.question("Upload these secrets? (y/N): ", resolve2);
525
+ });
526
+ rl.close();
527
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
528
+ console.log("Cancelled.");
529
+ process.exit(0);
530
+ }
531
+ }
532
+ console.log("\nUploading secrets...");
533
+ const mgmt = new SecretsManagement({
534
+ appId: options.project,
535
+ baseUrl: options.baseUrl
536
+ });
537
+ try {
538
+ const result = await mgmt.importSecrets({
539
+ version: "1.0",
540
+ origins,
541
+ secrets
542
+ });
543
+ console.log("\nSuccess!");
544
+ console.log(` Created: ${result.results.created} secrets`);
545
+ console.log(` Updated: ${result.results.updated} secrets`);
546
+ if (result.results.failed > 0) {
547
+ console.log(` Failed: ${result.results.failed} secrets`);
548
+ for (const failure of result.results.details.failed) {
549
+ console.log(` - ${failure.name}: ${failure.error}`);
550
+ }
551
+ }
552
+ if (result.sdkToken) {
553
+ console.log("\nSDK Token created:");
554
+ console.log(` ${result.sdkToken.token}`);
555
+ console.log("\n IMPORTANT: Save this token - it will not be shown again.");
556
+ console.log(" Add it to your static site code:");
557
+ console.log("");
558
+ console.log(" const sdk = new SecretsSDK();");
559
+ console.log(" // No appId or token needed - uses origin-based auth!");
560
+ }
561
+ console.log("\nYour site is ready!");
562
+ console.log(`Visit: https://ctklearn.carsontkempf.workers.dev to manage secrets`);
563
+ const configFile = "secrets-config.json";
564
+ const config = {
565
+ project: options.project,
566
+ origins
567
+ };
568
+ fs2.writeFileSync(configFile, JSON.stringify(config, null, 2));
569
+ console.log(`
570
+ Created local config: ${configFile}`);
571
+ } catch (error) {
572
+ console.error("\nUpload failed:", error.message);
573
+ if (error.status === 401) {
574
+ console.error('Your session may have expired. Try running "learn-secrets login" again.');
575
+ }
576
+ if (error.status === 404) {
577
+ console.error(`Project "${options.project}" not found. Check your project ID.`);
578
+ }
579
+ process.exit(1);
580
+ }
581
+ }
582
+
583
+ // src/cli/commands/setup.ts
584
+ async function setupCommand(options) {
585
+ const baseUrl = options.baseUrl || "https://ctklearn.carsontkempf.workers.dev";
586
+ const envFile = options.env || ".env";
587
+ const projectName = options.projectName || `Project - ${options.origin}`;
588
+ console.log("\n\u{1F680} Learn Secrets Setup\n");
589
+ console.log(`Email: ${options.email}`);
590
+ console.log(`Origin: ${options.origin}`);
591
+ console.log(`Project: ${projectName}`);
592
+ console.log(`Env File: ${envFile}
593
+ `);
594
+ let secrets;
595
+ try {
596
+ console.log(`\u{1F4D6} Reading ${envFile}...`);
597
+ const envVars = parseEnvFile(envFile);
598
+ secrets = detectApiKeys(envVars);
599
+ if (secrets.length === 0) {
600
+ console.log("\n\u26A0\uFE0F No API keys detected in .env file.");
601
+ console.log("Make sure your .env contains variables like:");
602
+ console.log(" OPENAI_API_KEY=sk-...");
603
+ console.log(" STRIPE_API_KEY=sk_live_...");
604
+ console.log(" SPOTIFY_CLIENT_ID=abc123...");
605
+ process.exit(0);
606
+ }
607
+ console.log("\n" + summarizeDetectedSecrets(secrets));
608
+ console.log("");
609
+ } catch (error) {
610
+ console.error(`\u274C Error reading ${envFile}:`, error.message);
611
+ process.exit(1);
612
+ }
613
+ if (!options.yes) {
614
+ const readline = await import("readline");
615
+ const rl = readline.createInterface({
616
+ input: process.stdin,
617
+ output: process.stdout
618
+ });
619
+ const answer = await new Promise((resolve2) => {
620
+ rl.question("Continue with setup? (y/N): ", resolve2);
621
+ });
622
+ rl.close();
623
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
624
+ console.log("\u274C Cancelled.");
625
+ process.exit(0);
626
+ }
627
+ }
628
+ console.log("\n\u{1F510} Authenticating...");
629
+ let cookies;
630
+ try {
631
+ const loginResponse = await fetch(`${baseUrl}/api/login`, {
632
+ method: "POST",
633
+ headers: {
634
+ "Content-Type": "application/json"
635
+ },
636
+ body: JSON.stringify({
637
+ email: options.email,
638
+ password: options.password
639
+ })
640
+ });
641
+ if (!loginResponse.ok) {
642
+ const errorData = await loginResponse.json().catch(() => ({}));
643
+ throw new Error(errorData.error || errorData.message || "Authentication failed");
644
+ }
645
+ const setCookieHeader = loginResponse.headers.get("set-cookie");
646
+ if (!setCookieHeader) {
647
+ throw new Error("No session cookie received");
648
+ }
649
+ const cookieParts = setCookieHeader.split(";")[0];
650
+ cookies = cookieParts;
651
+ console.log("\u2705 Authenticated successfully");
652
+ } catch (error) {
653
+ console.error(`\u274C Authentication failed: ${error.message}`);
654
+ console.log("\n\u{1F4A1} Make sure:");
655
+ console.log(" - Email and password are correct");
656
+ console.log(" - User account exists in the system");
657
+ console.log(" - Backend is accessible at", baseUrl);
658
+ process.exit(1);
659
+ }
660
+ console.log("\n\u{1F4E6} Creating project...");
661
+ let projectAppId;
662
+ try {
663
+ const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
664
+ method: "POST",
665
+ headers: {
666
+ "Content-Type": "application/json",
667
+ "Cookie": cookies
668
+ },
669
+ body: JSON.stringify({
670
+ name: projectName,
671
+ description: `Auto-provisioned for ${options.origin}`
672
+ })
673
+ });
674
+ if (!createProjectResponse.ok) {
675
+ const errorData = await createProjectResponse.json().catch(() => ({}));
676
+ throw new Error(errorData.message || "Failed to create project");
677
+ }
678
+ const projectData = await createProjectResponse.json();
679
+ projectAppId = projectData.project.appid;
680
+ console.log(`\u2705 Project created: ${projectAppId}`);
681
+ } catch (error) {
682
+ console.error(`\u274C Project creation failed: ${error.message}`);
683
+ process.exit(1);
684
+ }
685
+ console.log("\n\u{1F511} Generating SDK token...");
686
+ let sdkToken;
687
+ try {
688
+ const tokenResponse = await fetch(`${baseUrl}/api/projects/${projectAppId}/sdk-tokens`, {
689
+ method: "POST",
690
+ headers: {
691
+ "Content-Type": "application/json",
692
+ "Cookie": cookies
693
+ },
694
+ body: JSON.stringify({
695
+ label: `Production - ${options.origin}`,
696
+ origins: [options.origin],
697
+ expiresInDays: 365
698
+ })
699
+ });
700
+ if (!tokenResponse.ok) {
701
+ const errorData = await tokenResponse.json().catch(() => ({}));
702
+ throw new Error(errorData.message || "Failed to generate SDK token");
703
+ }
704
+ const tokenData = await tokenResponse.json();
705
+ sdkToken = tokenData.token;
706
+ console.log(`\u2705 SDK token generated`);
707
+ console.log(` Token: ${sdkToken.substring(0, 15)}...${sdkToken.slice(-4)}`);
708
+ } catch (error) {
709
+ console.error(`\u274C Token generation failed: ${error.message}`);
710
+ process.exit(1);
711
+ }
712
+ console.log("\n\u{1F4E4} Uploading secrets...");
713
+ try {
714
+ const importResponse = await fetch(`${baseUrl}/api/projects/${projectAppId}/import-secrets`, {
715
+ method: "POST",
716
+ headers: {
717
+ "Content-Type": "application/json",
718
+ "Cookie": cookies
719
+ },
720
+ body: JSON.stringify({
721
+ version: "1.0",
722
+ secrets
723
+ })
724
+ });
725
+ if (!importResponse.ok) {
726
+ const errorData = await importResponse.json().catch(() => ({}));
727
+ throw new Error(errorData.message || "Failed to upload secrets");
728
+ }
729
+ const result = await importResponse.json();
730
+ console.log("\u2705 Secrets uploaded:");
731
+ console.log(` Created: ${result.results.created}`);
732
+ console.log(` Updated: ${result.results.updated}`);
733
+ if (result.results.failed > 0) {
734
+ console.log(` \u26A0\uFE0F Failed: ${result.results.failed}`);
735
+ for (const failure of result.results.details.failed) {
736
+ console.log(` - ${failure.name}: ${failure.error}`);
737
+ }
738
+ }
739
+ } catch (error) {
740
+ console.error(`\u274C Upload failed: ${error.message}`);
741
+ }
742
+ console.log("\n" + "=".repeat(60));
743
+ console.log("\u2705 SETUP COMPLETE!");
744
+ console.log("=".repeat(60));
745
+ console.log("\nConfiguration:");
746
+ console.log(` LEARN_TENANT_URL: ${baseUrl}/api/auth`);
747
+ console.log(` LEARN_PROJECT_APPID: ${projectAppId}`);
748
+ console.log(` LEARN_SDK_TOKEN: ${sdkToken}`);
749
+ console.log(` LEARN_ORIGIN: ${options.origin}`);
750
+ console.log("\nUsage in your static site:");
751
+ console.log(' <script src="https://unpkg.com/learn-secrets-sdk/dist/index.global.js"></script>');
752
+ console.log(" <script>");
753
+ console.log(" // Zero-config mode - no tokens needed in client code!");
754
+ console.log(" const sdk = new SecretsSDK.SecretsSDK();");
755
+ console.log(` sdk.setAppId('${projectAppId}');`);
756
+ console.log(" ");
757
+ console.log(" // Use your secrets");
758
+ console.log(' const data = await sdk.get("newsapi", "/v2/top-headlines");');
759
+ console.log(" </script>");
760
+ console.log("\nManage secrets:");
761
+ console.log(` Visit: ${baseUrl}`);
762
+ console.log(` Or use CLI: learn-secrets init --project ${projectAppId} --origins ${options.origin}`);
763
+ console.log("\n" + "=".repeat(60));
764
+ const configPath = ".env.learn";
765
+ const configContent = `# Learn Platform Configuration
766
+ # Generated on ${(/* @__PURE__ */ new Date()).toISOString()}
767
+ # DO NOT COMMIT THIS FILE TO GIT
768
+
769
+ LEARN_TENANT_URL=${baseUrl}/api/auth
770
+ LEARN_PROJECT_APPID=${projectAppId}
771
+ LEARN_SDK_TOKEN=${sdkToken}
772
+ LEARN_ORIGIN=${options.origin}
773
+
774
+ # Token expires: 365 days
775
+ # Token prefix: ${sdkToken.slice(-4)}
776
+ `;
777
+ try {
778
+ const fs5 = await import("fs");
779
+ fs5.writeFileSync(configPath, configContent, "utf-8");
780
+ console.log(`
781
+ \u{1F4BE} Configuration saved to: ${configPath}`);
782
+ } catch (error) {
783
+ console.error(`
784
+ \u26A0\uFE0F Could not save configuration file: ${error.message}`);
785
+ }
786
+ console.log("\n\u{1F389} Your site is ready to use Learn Secrets!\n");
787
+ }
788
+
789
+ // src/cli/commands/config.ts
790
+ import fs3 from "fs";
791
+ async function configCommand(options) {
792
+ const configFile = "secrets-config.json";
793
+ if (!fs3.existsSync(configFile)) {
794
+ console.error("Run learn-secrets init first to generate config.");
795
+ return;
796
+ }
797
+ const config = JSON.parse(fs3.readFileSync(configFile, "utf8"));
798
+ if (options.origins) {
799
+ config.origins = options.origins.split(",").map((o) => o.trim());
800
+ console.log("Updated origins:", config.origins);
801
+ }
802
+ fs3.writeFileSync(configFile, JSON.stringify(config, null, 2));
803
+ }
804
+
805
+ // src/cli/commands/deploy.ts
806
+ import fs4 from "fs";
807
+ async function deployCommand() {
808
+ const configFile = "secrets-config.json";
809
+ if (!fs4.existsSync(configFile)) {
810
+ console.error("No config found.");
811
+ return;
812
+ }
813
+ const config = JSON.parse(fs4.readFileSync(configFile, "utf8"));
814
+ console.log("Deploying secrets config to backend:", config);
815
+ }
816
+
817
+ // src/cli/index.ts
818
+ var program = new Command();
819
+ program.name("learn-secrets").description("CLI tool for managing API secrets in static sites").version("1.5.0");
820
+ program.command("setup").description("Complete setup: create project, generate token, upload secrets").requiredOption("-e, --email <email>", "Your Learn account email").requiredOption("-p, --password <password>", "Your Learn account password").requiredOption("-o, --origin <domain>", "Your site origin (e.g., mysite.com)").option("--project-name <name>", 'Project name (default: "Project - <origin>")').option("--env <file>", "Path to .env file (default: .env)", ".env").option("--base-url <url>", "Custom base URL for API").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
821
+ await setupCommand(options);
822
+ });
823
+ program.command("login").description("Authenticate with Learn Secrets via browser").option("--base-url <url>", "Custom base URL for API").action(async (options) => {
824
+ await loginCommand(options);
825
+ });
826
+ program.command("init").description("Initialize secrets for a new site").requiredOption("-o, --origins <domains>", "Comma-separated list of allowed origins (e.g., mysite.com,localhost)").requiredOption("-p, --project <appid>", "Project app ID from dashboard").option("-e, --env <file>", "Path to .env file (default: .env)", ".env").option("--base-url <url>", "Custom base URL for API").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
827
+ await initCommand(options);
828
+ });
829
+ program.command("config").description("Configure allowed origins").option("--origins <origins>", "Comma separated origins").action(async (options) => {
830
+ await configCommand(options);
831
+ });
832
+ program.command("deploy").description("Deploy secrets config live").action(async () => {
833
+ await deployCommand();
834
+ });
835
+ program.parse(process.argv);
836
+ if (!process.argv.slice(2).length) {
837
+ program.outputHelp();
838
+ }