learn-secrets-sdk 1.10.0 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -4
- package/dist/browser.global.js +1 -0
- package/dist/chunk-Y4RHJLDQ.mjs +70 -0
- package/dist/cli/index.mjs +13 -329
- package/dist/env-resolver-FQMR3R4G.mjs +12 -0
- package/dist/index.mjs +849 -10
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,849 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import {
|
|
2
|
+
loadSecretsConfig,
|
|
3
|
+
resolveEnvInSecret,
|
|
4
|
+
resolveEnvInSecrets,
|
|
5
|
+
resolveEnvString
|
|
6
|
+
} from "./chunk-Y4RHJLDQ.mjs";
|
|
7
|
+
|
|
8
|
+
// src/types.ts
|
|
9
|
+
var SecretsSDKError = class extends Error {
|
|
10
|
+
constructor(message, status, response) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.response = response;
|
|
14
|
+
this.name = "SecretsSDKError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var OriginMismatchError = class extends SecretsSDKError {
|
|
18
|
+
constructor(message = "Origin not allowed for this token") {
|
|
19
|
+
super(message, 403);
|
|
20
|
+
this.name = "OriginMismatchError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var RateLimitError = class extends SecretsSDKError {
|
|
24
|
+
constructor(message = "Rate limit exceeded", retryAfter = 60, remaining = 0, limit = 100) {
|
|
25
|
+
super(message, 429);
|
|
26
|
+
this.name = "RateLimitError";
|
|
27
|
+
this.retryAfter = retryAfter;
|
|
28
|
+
this.remaining = remaining;
|
|
29
|
+
this.limit = limit;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var InvalidTokenError = class extends SecretsSDKError {
|
|
33
|
+
constructor(message = "Invalid or expired token") {
|
|
34
|
+
super(message, 401);
|
|
35
|
+
this.name = "InvalidTokenError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/client.ts
|
|
40
|
+
var SecretsSDK = class {
|
|
41
|
+
/**
|
|
42
|
+
* Create a new SecretsSDK instance
|
|
43
|
+
*
|
|
44
|
+
* Zero-config mode (recommended for static sites):
|
|
45
|
+
* const sdk = new SecretsSDK();
|
|
46
|
+
* // Origin header is used for authentication
|
|
47
|
+
*
|
|
48
|
+
* Token mode (for backward compatibility):
|
|
49
|
+
* const sdk = new SecretsSDK({ appId: '...', token: 'sk_live_...' });
|
|
50
|
+
*/
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.rateLimitInfo = null;
|
|
53
|
+
this.appId = options.appId || null;
|
|
54
|
+
this.token = options.token || options.sessionToken || null;
|
|
55
|
+
this.baseUrl = options.baseUrl || "https://cloudprototype.org";
|
|
56
|
+
this.timeout = options.timeout || 3e4;
|
|
57
|
+
this.retryOn429 = options.retryOn429 !== void 0 ? options.retryOn429 : true;
|
|
58
|
+
this.zeroConfigMode = !this.appId && !this.token;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get current rate limit information
|
|
62
|
+
*/
|
|
63
|
+
getUsage() {
|
|
64
|
+
return this.rateLimitInfo;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse rate limit headers from response
|
|
68
|
+
*/
|
|
69
|
+
parseRateLimitHeaders(headers) {
|
|
70
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
71
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
72
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
73
|
+
if (limit && remaining && reset) {
|
|
74
|
+
this.rateLimitInfo = {
|
|
75
|
+
limit: parseInt(limit),
|
|
76
|
+
remaining: parseInt(remaining),
|
|
77
|
+
reset: parseInt(reset)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Sleep utility for retry backoff
|
|
83
|
+
*/
|
|
84
|
+
sleep(ms) {
|
|
85
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Make fetch request with timeout
|
|
89
|
+
*/
|
|
90
|
+
async fetchWithTimeout(url, options, timeout) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(url, {
|
|
95
|
+
...options,
|
|
96
|
+
signal: controller.signal
|
|
97
|
+
});
|
|
98
|
+
return response;
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Call an external API securely through the proxy
|
|
105
|
+
* @param keyName - Name of the API key to use
|
|
106
|
+
* @param endpoint - API endpoint path (e.g., '/v1/chat/completions')
|
|
107
|
+
* @param options - Request options (method, body, headers)
|
|
108
|
+
* @returns Promise with the API response
|
|
109
|
+
*/
|
|
110
|
+
async call(keyName, endpoint, options = {}) {
|
|
111
|
+
let retries = 0;
|
|
112
|
+
const maxRetries = this.retryOn429 ? 3 : 0;
|
|
113
|
+
while (true) {
|
|
114
|
+
try {
|
|
115
|
+
const proxyUrl = this.zeroConfigMode ? `${this.baseUrl}/api/sdk/proxy` : `${this.baseUrl}/api/sdk/${this.appId}/proxy`;
|
|
116
|
+
const requestHeaders = {
|
|
117
|
+
"Content-Type": "application/json"
|
|
118
|
+
};
|
|
119
|
+
if (!this.zeroConfigMode && this.token) {
|
|
120
|
+
requestHeaders["Authorization"] = `Bearer ${this.token}`;
|
|
121
|
+
}
|
|
122
|
+
const response = await this.fetchWithTimeout(
|
|
123
|
+
proxyUrl,
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: requestHeaders,
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
keyName,
|
|
129
|
+
endpoint,
|
|
130
|
+
method: options.method || "GET",
|
|
131
|
+
body: options.body,
|
|
132
|
+
headers: options.headers
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
this.timeout
|
|
136
|
+
);
|
|
137
|
+
this.parseRateLimitHeaders(response.headers);
|
|
138
|
+
const result = await response.json();
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const errorMessage = result.data?.message || result?.message || `Request failed with status ${response.status}`;
|
|
141
|
+
if (response.status === 403 && errorMessage.includes("Origin")) {
|
|
142
|
+
throw new OriginMismatchError(errorMessage);
|
|
143
|
+
}
|
|
144
|
+
if (response.status === 401) {
|
|
145
|
+
throw new InvalidTokenError(errorMessage);
|
|
146
|
+
}
|
|
147
|
+
if (response.status === 429) {
|
|
148
|
+
const retryAfter = this.rateLimitInfo ? this.rateLimitInfo.reset - Math.floor(Date.now() / 1e3) : 60;
|
|
149
|
+
const error = new RateLimitError(
|
|
150
|
+
errorMessage,
|
|
151
|
+
retryAfter,
|
|
152
|
+
this.rateLimitInfo?.remaining || 0,
|
|
153
|
+
this.rateLimitInfo?.limit || 100
|
|
154
|
+
);
|
|
155
|
+
if (this.retryOn429 && retries < maxRetries) {
|
|
156
|
+
retries++;
|
|
157
|
+
const backoffMs = Math.min(1e3 * Math.pow(2, retries - 1), 8e3);
|
|
158
|
+
await this.sleep(backoffMs);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
throw new SecretsSDKError(errorMessage, response.status, result);
|
|
164
|
+
}
|
|
165
|
+
return result.data;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof SecretsSDKError) {
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
171
|
+
throw new SecretsSDKError("Request timeout", 408);
|
|
172
|
+
}
|
|
173
|
+
throw new SecretsSDKError(
|
|
174
|
+
err instanceof Error ? err.message : "Unknown error occurred",
|
|
175
|
+
500
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Make a GET request
|
|
182
|
+
*/
|
|
183
|
+
async get(keyName, endpoint, headers) {
|
|
184
|
+
return this.call(keyName, endpoint, { method: "GET", headers });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Make a POST request
|
|
188
|
+
*/
|
|
189
|
+
async post(keyName, endpoint, body, headers) {
|
|
190
|
+
return this.call(keyName, endpoint, { method: "POST", body, headers });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Make a PUT request
|
|
194
|
+
*/
|
|
195
|
+
async put(keyName, endpoint, body, headers) {
|
|
196
|
+
return this.call(keyName, endpoint, { method: "PUT", body, headers });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Make a DELETE request
|
|
200
|
+
*/
|
|
201
|
+
async delete(keyName, endpoint, headers) {
|
|
202
|
+
return this.call(keyName, endpoint, { method: "DELETE", headers });
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Make a PATCH request
|
|
206
|
+
*/
|
|
207
|
+
async patch(keyName, endpoint, body, headers) {
|
|
208
|
+
return this.call(keyName, endpoint, { method: "PATCH", body, headers });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Secrets Management API (zero-config mode)
|
|
212
|
+
* These methods allow programmatic CRUD operations on project secrets
|
|
213
|
+
* Requires appId to be set (can be set in constructor or via setAppId)
|
|
214
|
+
*/
|
|
215
|
+
/**
|
|
216
|
+
* Set App ID for secrets management
|
|
217
|
+
* Required before calling secrets management methods
|
|
218
|
+
*/
|
|
219
|
+
setAppId(appId) {
|
|
220
|
+
this.appId = appId;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Make authenticated request to secrets management API
|
|
224
|
+
*/
|
|
225
|
+
async secretsRequest(action, body) {
|
|
226
|
+
if (!this.appId) {
|
|
227
|
+
throw new SecretsSDKError("App ID required for secrets management. Call setAppId() first.", 400);
|
|
228
|
+
}
|
|
229
|
+
const response = await this.fetchWithTimeout(
|
|
230
|
+
`${this.baseUrl}/api/projects/${this.appId}/secrets/manage?action=${action}`,
|
|
231
|
+
{
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
"Content-Type": "application/json"
|
|
235
|
+
},
|
|
236
|
+
body: body ? JSON.stringify(body) : void 0
|
|
237
|
+
},
|
|
238
|
+
this.timeout
|
|
239
|
+
);
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
const errorData = await response.json().catch(() => ({}));
|
|
242
|
+
throw new SecretsSDKError(
|
|
243
|
+
errorData.message || `Secrets management failed: ${response.statusText}`,
|
|
244
|
+
response.status,
|
|
245
|
+
errorData
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return response.json();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* List all secrets for the project
|
|
252
|
+
* Returns secret metadata (no API keys)
|
|
253
|
+
*/
|
|
254
|
+
async listSecrets() {
|
|
255
|
+
return this.secretsRequest("list");
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create a new secret
|
|
259
|
+
*/
|
|
260
|
+
async createSecret(secret) {
|
|
261
|
+
return this.secretsRequest("create", secret);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Update an existing secret
|
|
265
|
+
*/
|
|
266
|
+
async updateSecret(update) {
|
|
267
|
+
return this.secretsRequest("update", update);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Delete a secret
|
|
271
|
+
*/
|
|
272
|
+
async deleteSecret(name) {
|
|
273
|
+
return this.secretsRequest("delete", { name });
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Sync secrets from .env file format
|
|
277
|
+
* Bulk upload/update secrets
|
|
278
|
+
*/
|
|
279
|
+
async syncEnv(secrets, provider) {
|
|
280
|
+
return this.secretsRequest("sync", { secrets, provider });
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/credentials.ts
|
|
285
|
+
import * as fs from "fs";
|
|
286
|
+
import * as path from "path";
|
|
287
|
+
import * as os from "os";
|
|
288
|
+
function getCredentialsPath() {
|
|
289
|
+
const homeDir = os.homedir();
|
|
290
|
+
const learnDir = path.join(homeDir, ".learn");
|
|
291
|
+
return path.join(learnDir, "credentials.json");
|
|
292
|
+
}
|
|
293
|
+
function ensureLearnDir() {
|
|
294
|
+
const homeDir = os.homedir();
|
|
295
|
+
const learnDir = path.join(homeDir, ".learn");
|
|
296
|
+
if (!fs.existsSync(learnDir)) {
|
|
297
|
+
fs.mkdirSync(learnDir, { recursive: true, mode: 448 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function readAllCredentials() {
|
|
301
|
+
try {
|
|
302
|
+
const credPath = getCredentialsPath();
|
|
303
|
+
if (!fs.existsSync(credPath)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const data = fs.readFileSync(credPath, "utf-8");
|
|
307
|
+
const store = JSON.parse(data);
|
|
308
|
+
if (store.access_token && !store["learn-secrets-sdk"] && !store["learn-auth-sdk"]) {
|
|
309
|
+
return {
|
|
310
|
+
"learn-secrets-sdk": store
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return store;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function writeAllCredentials(store) {
|
|
319
|
+
ensureLearnDir();
|
|
320
|
+
const credPath = getCredentialsPath();
|
|
321
|
+
const data = JSON.stringify(store, null, 2);
|
|
322
|
+
fs.writeFileSync(credPath, data, { mode: 384 });
|
|
323
|
+
}
|
|
324
|
+
function readSDKCredentials(sdkName) {
|
|
325
|
+
const store = readAllCredentials();
|
|
326
|
+
if (!store) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return store[sdkName] || null;
|
|
330
|
+
}
|
|
331
|
+
function writeSDKCredentials(sdkName, creds) {
|
|
332
|
+
const store = readAllCredentials() || {};
|
|
333
|
+
store[sdkName] = creds;
|
|
334
|
+
writeAllCredentials(store);
|
|
335
|
+
}
|
|
336
|
+
function deleteSDKCredentials(sdkName) {
|
|
337
|
+
const store = readAllCredentials();
|
|
338
|
+
if (!store) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
delete store[sdkName];
|
|
342
|
+
if (Object.keys(store).length === 0) {
|
|
343
|
+
deleteCredentials();
|
|
344
|
+
} else {
|
|
345
|
+
writeAllCredentials(store);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function readCredentials() {
|
|
349
|
+
return readSDKCredentials("learn-secrets-sdk");
|
|
350
|
+
}
|
|
351
|
+
function deleteCredentials() {
|
|
352
|
+
try {
|
|
353
|
+
const credPath = getCredentialsPath();
|
|
354
|
+
if (fs.existsSync(credPath)) {
|
|
355
|
+
fs.unlinkSync(credPath);
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function hasSDKCredentials(sdkName) {
|
|
361
|
+
const creds = readSDKCredentials(sdkName);
|
|
362
|
+
return creds !== null && !!creds.access_token;
|
|
363
|
+
}
|
|
364
|
+
function hasValidCredentials() {
|
|
365
|
+
const creds = readCredentials();
|
|
366
|
+
if (!creds) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
if (!creds.expires_at) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
const expiresAt = new Date(creds.expires_at);
|
|
373
|
+
const now = /* @__PURE__ */ new Date();
|
|
374
|
+
return expiresAt > now;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/machine-id.ts
|
|
378
|
+
import { exec } from "child_process";
|
|
379
|
+
import { promisify } from "util";
|
|
380
|
+
import { createHash } from "crypto";
|
|
381
|
+
import * as os2 from "os";
|
|
382
|
+
var execAsync = promisify(exec);
|
|
383
|
+
async function getMachineId() {
|
|
384
|
+
const platform2 = process.platform;
|
|
385
|
+
try {
|
|
386
|
+
let rawId;
|
|
387
|
+
if (platform2 === "darwin") {
|
|
388
|
+
rawId = await getMacOSId();
|
|
389
|
+
} else if (platform2 === "linux") {
|
|
390
|
+
rawId = await getLinuxId();
|
|
391
|
+
} else if (platform2 === "win32") {
|
|
392
|
+
rawId = await getWindowsId();
|
|
393
|
+
} else {
|
|
394
|
+
throw new Error(`Unsupported platform: ${platform2}`);
|
|
395
|
+
}
|
|
396
|
+
const machineId = createHash("sha256").update(rawId).digest("hex").substring(0, 32);
|
|
397
|
+
const fingerprintData = JSON.stringify({
|
|
398
|
+
machineId,
|
|
399
|
+
cpus: os2.cpus().length,
|
|
400
|
+
arch: os2.arch(),
|
|
401
|
+
platform: os2.platform(),
|
|
402
|
+
homedir: os2.homedir()
|
|
403
|
+
});
|
|
404
|
+
const fingerprint = createHash("sha256").update(fingerprintData).digest("hex");
|
|
405
|
+
return { machineId, fingerprint };
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw new Error(`Failed to get machine ID: ${error.message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function getMacOSId() {
|
|
411
|
+
try {
|
|
412
|
+
const { stdout: serial } = await execAsync(
|
|
413
|
+
`ioreg -l | grep IOPlatformSerialNumber | awk '{print $4}' | sed 's/"//g'`
|
|
414
|
+
);
|
|
415
|
+
const { stdout: hostname } = await execAsync("hostname");
|
|
416
|
+
const serialTrimmed = serial.trim();
|
|
417
|
+
const hostnameTrimmed = hostname.trim();
|
|
418
|
+
if (serialTrimmed && hostnameTrimmed) {
|
|
419
|
+
return `${serialTrimmed}-${hostnameTrimmed}`;
|
|
420
|
+
}
|
|
421
|
+
const { stdout: uuid } = await execAsync(
|
|
422
|
+
`ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID | awk '{print $3}' | sed 's/"//g'`
|
|
423
|
+
);
|
|
424
|
+
return uuid.trim();
|
|
425
|
+
} catch (error) {
|
|
426
|
+
throw new Error("Failed to get macOS machine ID");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function getLinuxId() {
|
|
430
|
+
try {
|
|
431
|
+
const { stdout } = await execAsync(
|
|
432
|
+
"cat /etc/machine-id 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null"
|
|
433
|
+
);
|
|
434
|
+
const machineId = stdout.trim();
|
|
435
|
+
if (machineId) {
|
|
436
|
+
return machineId;
|
|
437
|
+
}
|
|
438
|
+
throw new Error("Machine ID file not found");
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new Error("Failed to get Linux machine ID");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function getWindowsId() {
|
|
444
|
+
try {
|
|
445
|
+
const { stdout } = await execAsync("wmic csproduct get uuid");
|
|
446
|
+
const lines = stdout.split("\n").map((l) => l.trim());
|
|
447
|
+
const uuid = lines.find((l) => l && l !== "UUID");
|
|
448
|
+
if (uuid) {
|
|
449
|
+
return uuid;
|
|
450
|
+
}
|
|
451
|
+
throw new Error("Failed to parse machine UUID from WMIC");
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error("Failed to get Windows machine ID");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/management.ts
|
|
458
|
+
var SecretsManagement = class {
|
|
459
|
+
constructor(options) {
|
|
460
|
+
this.appId = options.appId;
|
|
461
|
+
this.baseUrl = options.baseUrl || "https://cloudprototype.org";
|
|
462
|
+
this.timeout = options.timeout || 3e4;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Open a URL in the default browser
|
|
466
|
+
*/
|
|
467
|
+
async openBrowser(url) {
|
|
468
|
+
const { exec: exec2 } = await import("child_process");
|
|
469
|
+
const { promisify: promisify2 } = await import("util");
|
|
470
|
+
const execAsync2 = promisify2(exec2);
|
|
471
|
+
const platform2 = process.platform;
|
|
472
|
+
try {
|
|
473
|
+
if (platform2 === "darwin") {
|
|
474
|
+
await execAsync2(`open "${url}"`);
|
|
475
|
+
} else if (platform2 === "win32") {
|
|
476
|
+
await execAsync2(`start "${url}"`);
|
|
477
|
+
} else {
|
|
478
|
+
await execAsync2(`xdg-open "${url}"`);
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.error("Failed to open browser:", err);
|
|
482
|
+
throw new SecretsSDKError("Failed to open browser", 500);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Sleep for specified milliseconds
|
|
487
|
+
*/
|
|
488
|
+
sleep(ms) {
|
|
489
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Browser OAuth login flow with 2FA
|
|
493
|
+
* Opens browser for user to authorize, then prompts for 2FA code
|
|
494
|
+
*/
|
|
495
|
+
async login() {
|
|
496
|
+
const SDK_NAME = "learn-secrets-sdk";
|
|
497
|
+
try {
|
|
498
|
+
console.log(`Authenticating with Learn (${SDK_NAME})...
|
|
499
|
+
`);
|
|
500
|
+
const machineInfo = await getMachineId();
|
|
501
|
+
const existingCreds = readSDKCredentials(SDK_NAME);
|
|
502
|
+
if (existingCreds && existingCreds.access_token) {
|
|
503
|
+
const checkResponse = await fetch(`${this.baseUrl}/api/auth/cli/check-authorization`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
"Content-Type": "application/json"
|
|
507
|
+
},
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
machine_id: machineInfo.machineId,
|
|
510
|
+
sdk_name: SDK_NAME
|
|
511
|
+
})
|
|
512
|
+
});
|
|
513
|
+
if (checkResponse.ok) {
|
|
514
|
+
const checkData = await checkResponse.json();
|
|
515
|
+
if (checkData.authorized) {
|
|
516
|
+
console.log("This machine is already authorized.");
|
|
517
|
+
console.log(`Logged in as: ${checkData.user_email || checkData.user_id}`);
|
|
518
|
+
return {
|
|
519
|
+
success: true,
|
|
520
|
+
user: checkData.user_email || checkData.user_id
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const deviceResponse = await fetch(`${this.baseUrl}/api/auth/cli/device`, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: {
|
|
528
|
+
"Content-Type": "application/json"
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
if (!deviceResponse.ok) {
|
|
532
|
+
throw new SecretsSDKError(
|
|
533
|
+
"Failed to generate device code",
|
|
534
|
+
deviceResponse.status
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const deviceData = await deviceResponse.json();
|
|
538
|
+
console.log(`
|
|
539
|
+
Log in on ${this.baseUrl}`);
|
|
540
|
+
console.log("Login at:");
|
|
541
|
+
console.log(deviceData.verification_uri_complete);
|
|
542
|
+
console.log("\nPress ENTER to open in the browser...");
|
|
543
|
+
const readline = await import("readline");
|
|
544
|
+
const rl = readline.createInterface({
|
|
545
|
+
input: process.stdin,
|
|
546
|
+
output: process.stdout
|
|
547
|
+
});
|
|
548
|
+
await new Promise((resolve) => {
|
|
549
|
+
rl.question("", () => {
|
|
550
|
+
rl.close();
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
console.log("Opening browser...\n");
|
|
555
|
+
await this.openBrowser(deviceData.verification_uri_complete);
|
|
556
|
+
let tokenData = null;
|
|
557
|
+
const interval = deviceData.interval * 1e3;
|
|
558
|
+
const maxAttempts = Math.ceil(deviceData.expires_in / deviceData.interval);
|
|
559
|
+
let attempts = 0;
|
|
560
|
+
while (attempts < maxAttempts) {
|
|
561
|
+
await this.sleep(interval);
|
|
562
|
+
attempts++;
|
|
563
|
+
const tokenResponse = await fetch(`${this.baseUrl}/api/auth/cli/token`, {
|
|
564
|
+
method: "POST",
|
|
565
|
+
headers: {
|
|
566
|
+
"Content-Type": "application/json"
|
|
567
|
+
},
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
device_code: deviceData.device_code
|
|
570
|
+
})
|
|
571
|
+
});
|
|
572
|
+
if (tokenResponse.status === 202) {
|
|
573
|
+
process.stdout.write(".");
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (tokenResponse.ok) {
|
|
577
|
+
tokenData = await tokenResponse.json();
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (!tokenData) {
|
|
582
|
+
throw new SecretsSDKError("Device authorization timeout", 408);
|
|
583
|
+
}
|
|
584
|
+
console.log("\n\nDevice authorized! Retrieving access token...");
|
|
585
|
+
writeSDKCredentials(SDK_NAME, {
|
|
586
|
+
access_token: tokenData.access_token,
|
|
587
|
+
refresh_token: null,
|
|
588
|
+
expires_at: null,
|
|
589
|
+
user_id: tokenData.user_id,
|
|
590
|
+
machine_id: machineInfo.machineId,
|
|
591
|
+
sdk_name: SDK_NAME,
|
|
592
|
+
authorized_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
593
|
+
});
|
|
594
|
+
console.log("\nAuthentication successful!");
|
|
595
|
+
console.log(`Logged in as: ${tokenData.user_email || tokenData.user_id}`);
|
|
596
|
+
console.log("This machine is now permanently authorized.\n");
|
|
597
|
+
return {
|
|
598
|
+
success: true,
|
|
599
|
+
user: tokenData.user_email || tokenData.user_id
|
|
600
|
+
};
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (err instanceof SecretsSDKError) {
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
throw new SecretsSDKError(
|
|
606
|
+
err.message || "Login failed",
|
|
607
|
+
err.status || 500
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Check if user is authenticated
|
|
613
|
+
*/
|
|
614
|
+
async isAuthenticated() {
|
|
615
|
+
return hasValidCredentials();
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Logout - clear stored credentials
|
|
619
|
+
*/
|
|
620
|
+
logout() {
|
|
621
|
+
deleteCredentials();
|
|
622
|
+
console.log("Logged out successfully");
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get access token from stored credentials
|
|
626
|
+
* Throws error if not authenticated
|
|
627
|
+
*/
|
|
628
|
+
getAccessToken() {
|
|
629
|
+
const creds = readCredentials();
|
|
630
|
+
if (!creds) {
|
|
631
|
+
throw new SecretsSDKError("Not authenticated. Run login() first.", 401);
|
|
632
|
+
}
|
|
633
|
+
if (creds.expires_at) {
|
|
634
|
+
const expiresAt = new Date(creds.expires_at);
|
|
635
|
+
const now = /* @__PURE__ */ new Date();
|
|
636
|
+
if (expiresAt <= now) {
|
|
637
|
+
throw new SecretsSDKError("Token expired. Run login() again.", 401);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return creds.access_token;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Make authenticated request
|
|
644
|
+
*/
|
|
645
|
+
async request(method, path2, body) {
|
|
646
|
+
const token = this.getAccessToken();
|
|
647
|
+
const options = {
|
|
648
|
+
method,
|
|
649
|
+
headers: {
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
Authorization: `Bearer ${token}`
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
if (body) {
|
|
655
|
+
options.body = JSON.stringify(body);
|
|
656
|
+
}
|
|
657
|
+
const response = await fetch(`${this.baseUrl}${path2}`, options);
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
const errorData = await response.json().catch(() => ({}));
|
|
660
|
+
throw new SecretsSDKError(
|
|
661
|
+
errorData.message || `Request failed: ${response.statusText}`,
|
|
662
|
+
response.status,
|
|
663
|
+
errorData
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return response.json();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* List all secrets (with masked API keys)
|
|
670
|
+
*/
|
|
671
|
+
async list() {
|
|
672
|
+
const response = await this.request(
|
|
673
|
+
"GET",
|
|
674
|
+
`/api/projects/${this.appId}/secrets`
|
|
675
|
+
);
|
|
676
|
+
return response.secrets;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Sync secrets from config
|
|
680
|
+
*/
|
|
681
|
+
async sync(secrets, options) {
|
|
682
|
+
const response = await this.request(
|
|
683
|
+
"POST",
|
|
684
|
+
`/api/projects/${this.appId}/secrets/sync`,
|
|
685
|
+
{
|
|
686
|
+
secrets,
|
|
687
|
+
options: {
|
|
688
|
+
deleteMissing: options?.deleteMissing ?? false,
|
|
689
|
+
dryRun: options?.dryRun ?? false
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
return response;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Import secrets from a config object (bulk import)
|
|
697
|
+
* Also creates SDK token if origins are provided
|
|
698
|
+
*
|
|
699
|
+
* @param config - The secrets configuration
|
|
700
|
+
* @returns Import results including created/updated counts and optional SDK token
|
|
701
|
+
*/
|
|
702
|
+
async importSecrets(config) {
|
|
703
|
+
return this.request(
|
|
704
|
+
"POST",
|
|
705
|
+
`/api/projects/${this.appId}/import-secrets`,
|
|
706
|
+
{
|
|
707
|
+
version: config.version || "1.0",
|
|
708
|
+
origins: config.origins,
|
|
709
|
+
secrets: config.secrets
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Import secrets from a JSON file path
|
|
715
|
+
* Resolves environment variables in the config
|
|
716
|
+
*/
|
|
717
|
+
async importFromFile(configPath) {
|
|
718
|
+
const { loadSecretsConfig: loadSecretsConfig2 } = await import("./env-resolver-FQMR3R4G.mjs");
|
|
719
|
+
const config = loadSecretsConfig2(configPath);
|
|
720
|
+
return this.importSecrets(config);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/worker-client.ts
|
|
725
|
+
var WorkerClient = class {
|
|
726
|
+
constructor(options) {
|
|
727
|
+
this.workerUrl = options.workerUrl.replace(/\/$/, "");
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get public configuration
|
|
731
|
+
*
|
|
732
|
+
* Returns only PUBLIC_* environment variables from Worker
|
|
733
|
+
* Safe for frontend use
|
|
734
|
+
*/
|
|
735
|
+
async getConfig() {
|
|
736
|
+
const response = await fetch(`${this.workerUrl}/config`, {
|
|
737
|
+
method: "GET",
|
|
738
|
+
credentials: "include"
|
|
739
|
+
});
|
|
740
|
+
if (!response.ok) {
|
|
741
|
+
throw new Error(`Failed to get config: ${response.status} ${response.statusText}`);
|
|
742
|
+
}
|
|
743
|
+
const data = await response.json();
|
|
744
|
+
return data.public || {};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Proxy API request with secret injection
|
|
748
|
+
*
|
|
749
|
+
* Makes API call through Worker which injects secret server-side
|
|
750
|
+
* Secret never reaches browser
|
|
751
|
+
*
|
|
752
|
+
* @param request - Proxy request configuration
|
|
753
|
+
* @returns API response data
|
|
754
|
+
*/
|
|
755
|
+
async proxy(request) {
|
|
756
|
+
const response = await fetch(`${this.workerUrl}/proxy`, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: {
|
|
759
|
+
"Content-Type": "application/json"
|
|
760
|
+
},
|
|
761
|
+
credentials: "include",
|
|
762
|
+
body: JSON.stringify(request)
|
|
763
|
+
});
|
|
764
|
+
if (!response.ok) {
|
|
765
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
766
|
+
throw new Error(error.error || `Proxy request failed: ${response.status}`);
|
|
767
|
+
}
|
|
768
|
+
return response.json();
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Generate Apple Music JWT
|
|
772
|
+
*
|
|
773
|
+
* Private key stays in Worker, only JWT returned to frontend
|
|
774
|
+
*
|
|
775
|
+
* @param request - Apple JWT request with secret name
|
|
776
|
+
* @returns JWT token
|
|
777
|
+
*/
|
|
778
|
+
async generateAppleJwt(request) {
|
|
779
|
+
const response = await fetch(`${this.workerUrl}/auth/apple-jwt`, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
headers: {
|
|
782
|
+
"Content-Type": "application/json"
|
|
783
|
+
},
|
|
784
|
+
credentials: "include",
|
|
785
|
+
body: JSON.stringify(request)
|
|
786
|
+
});
|
|
787
|
+
if (!response.ok) {
|
|
788
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
789
|
+
throw new Error(error.error || `Apple JWT generation failed: ${response.status}`);
|
|
790
|
+
}
|
|
791
|
+
const data = await response.json();
|
|
792
|
+
return data.token;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Exchange OAuth authorization code for access token
|
|
796
|
+
*
|
|
797
|
+
* Client secret stays in Worker, tokens returned to frontend
|
|
798
|
+
*
|
|
799
|
+
* @param request - OAuth exchange request
|
|
800
|
+
* @returns OAuth token response
|
|
801
|
+
*/
|
|
802
|
+
async exchangeOAuthCode(request) {
|
|
803
|
+
const response = await fetch(`${this.workerUrl}/auth/oauth`, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: {
|
|
806
|
+
"Content-Type": "application/json"
|
|
807
|
+
},
|
|
808
|
+
credentials: "include",
|
|
809
|
+
body: JSON.stringify(request)
|
|
810
|
+
});
|
|
811
|
+
if (!response.ok) {
|
|
812
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
813
|
+
throw new Error(error.error || `OAuth exchange failed: ${response.status}`);
|
|
814
|
+
}
|
|
815
|
+
return response.json();
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Health check
|
|
819
|
+
*/
|
|
820
|
+
async health() {
|
|
821
|
+
const response = await fetch(`${this.workerUrl}/health`, {
|
|
822
|
+
method: "GET"
|
|
823
|
+
});
|
|
824
|
+
if (!response.ok) {
|
|
825
|
+
throw new Error(`Health check failed: ${response.status}`);
|
|
826
|
+
}
|
|
827
|
+
return response.json();
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
export {
|
|
831
|
+
InvalidTokenError,
|
|
832
|
+
OriginMismatchError,
|
|
833
|
+
RateLimitError,
|
|
834
|
+
SecretsManagement,
|
|
835
|
+
SecretsSDK,
|
|
836
|
+
SecretsSDKError,
|
|
837
|
+
WorkerClient,
|
|
838
|
+
deleteSDKCredentials,
|
|
839
|
+
getMachineId,
|
|
840
|
+
hasSDKCredentials,
|
|
841
|
+
loadSecretsConfig,
|
|
842
|
+
readAllCredentials,
|
|
843
|
+
readSDKCredentials,
|
|
844
|
+
resolveEnvInSecret,
|
|
845
|
+
resolveEnvInSecrets,
|
|
846
|
+
resolveEnvString,
|
|
847
|
+
writeAllCredentials,
|
|
848
|
+
writeSDKCredentials
|
|
849
|
+
};
|