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