@keywaysh/cli 0.0.12 → 0.0.14
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 +5 -4
- package/dist/cli.js +107 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,19 +162,20 @@ keyway logout
|
|
|
162
162
|
Keyway is designed to be **simple and secure** — a major upgrade from Slack or Notion, without the complexity of Hashicorp Vault or AWS Secrets Manager.
|
|
163
163
|
|
|
164
164
|
**What we do:**
|
|
165
|
-
- AES-256-GCM encryption server-side
|
|
166
|
-
- TLS everywhere
|
|
165
|
+
- AES-256-GCM encryption server-side and client-side token storage
|
|
166
|
+
- TLS everywhere (HTTPS enforced)
|
|
167
167
|
- GitHub read-only permissions
|
|
168
168
|
- No access to your code
|
|
169
169
|
- Secrets stored encrypted at rest
|
|
170
170
|
- No analytics on secret values (only metadata)
|
|
171
|
+
- Encrypted token storage with file permissions
|
|
171
172
|
|
|
172
173
|
**What we don't do:**
|
|
173
174
|
- No zero-trust enterprise model
|
|
174
175
|
- No access to your cloud infrastructure
|
|
175
176
|
- No access to your production deployment keys
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
For detailed security information, see [SECURITY.md](./SECURITY.md) and [keyway.sh/security](https://keyway.sh/security)
|
|
178
179
|
|
|
179
180
|
## Who is this for?
|
|
180
181
|
|
|
@@ -293,7 +294,7 @@ Your team stays perfectly in sync.
|
|
|
293
294
|
## Support
|
|
294
295
|
|
|
295
296
|
- **Issues**: [github.com/keywaysh/cli/issues](https://github.com/keywaysh/cli/issues)
|
|
296
|
-
- **Email**:
|
|
297
|
+
- **Email**: hello@keyway.sh
|
|
297
298
|
- **Website**: [keyway.sh](https://keyway.sh)
|
|
298
299
|
|
|
299
300
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -66,10 +66,10 @@ var INTERNAL_API_URL = "https://api.keyway.sh";
|
|
|
66
66
|
var INTERNAL_POSTHOG_KEY = "phc_duG0qqI5z8LeHrS9pNxR5KaD4djgD0nmzUxuD3zP0ov";
|
|
67
67
|
var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
68
68
|
|
|
69
|
-
// package.json
|
|
69
|
+
// package.json
|
|
70
70
|
var package_default = {
|
|
71
71
|
name: "@keywaysh/cli",
|
|
72
|
-
version: "0.0.
|
|
72
|
+
version: "0.0.14",
|
|
73
73
|
description: "One link to all your secrets",
|
|
74
74
|
type: "module",
|
|
75
75
|
bin: {
|
|
@@ -132,11 +132,32 @@ var package_default = {
|
|
|
132
132
|
// src/utils/api.ts
|
|
133
133
|
var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
|
|
134
134
|
var USER_AGENT = `keyway-cli/${package_default.version}`;
|
|
135
|
+
function validateApiUrl(url) {
|
|
136
|
+
const parsed = new URL(url);
|
|
137
|
+
if (parsed.protocol !== "https:") {
|
|
138
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "0.0.0.0";
|
|
139
|
+
if (!isLocalhost) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Insecure API URL detected: ${url}
|
|
142
|
+
HTTPS is required for security. If this is a development server, use localhost or configure HTTPS.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (!process.env.KEYWAY_DISABLE_SECURITY_WARNINGS) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`\u26A0\uFE0F WARNING: Using insecure HTTP connection to ${url}
|
|
148
|
+
This should only be used for local development.
|
|
149
|
+
Set KEYWAY_DISABLE_SECURITY_WARNINGS=1 to suppress this warning.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
validateApiUrl(API_BASE_URL);
|
|
135
155
|
var APIError = class extends Error {
|
|
136
|
-
constructor(statusCode, error, message) {
|
|
156
|
+
constructor(statusCode, error, message, upgradeUrl) {
|
|
137
157
|
super(message);
|
|
138
158
|
this.statusCode = statusCode;
|
|
139
159
|
this.error = error;
|
|
160
|
+
this.upgradeUrl = upgradeUrl;
|
|
140
161
|
this.name = "APIError";
|
|
141
162
|
}
|
|
142
163
|
};
|
|
@@ -147,8 +168,9 @@ async function handleResponse(response) {
|
|
|
147
168
|
if (contentType.includes("application/json")) {
|
|
148
169
|
try {
|
|
149
170
|
const error = JSON.parse(text);
|
|
150
|
-
throw new APIError(response.status, error.error, error.message);
|
|
151
|
-
} catch {
|
|
171
|
+
throw new APIError(response.status, error.error, error.message, error.upgrade_url);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (e instanceof APIError) throw e;
|
|
152
174
|
throw new APIError(response.status, "http_error", text || `HTTP ${response.status}`);
|
|
153
175
|
}
|
|
154
176
|
}
|
|
@@ -277,71 +299,6 @@ import crypto from "crypto";
|
|
|
277
299
|
import path from "path";
|
|
278
300
|
import os from "os";
|
|
279
301
|
import fs from "fs";
|
|
280
|
-
|
|
281
|
-
// package.json
|
|
282
|
-
var package_default2 = {
|
|
283
|
-
name: "@keywaysh/cli",
|
|
284
|
-
version: "0.0.12",
|
|
285
|
-
description: "One link to all your secrets",
|
|
286
|
-
type: "module",
|
|
287
|
-
bin: {
|
|
288
|
-
keyway: "./dist/cli.js"
|
|
289
|
-
},
|
|
290
|
-
main: "./dist/cli.js",
|
|
291
|
-
files: [
|
|
292
|
-
"dist"
|
|
293
|
-
],
|
|
294
|
-
scripts: {
|
|
295
|
-
dev: "pnpm exec tsx src/cli.ts",
|
|
296
|
-
build: "pnpm exec tsup",
|
|
297
|
-
"build:watch": "pnpm exec tsup --watch",
|
|
298
|
-
prepublishOnly: "pnpm run build",
|
|
299
|
-
test: "pnpm exec vitest run",
|
|
300
|
-
"test:watch": "pnpm exec vitest",
|
|
301
|
-
release: "npm version patch && git push && git push --tags",
|
|
302
|
-
"release:minor": "npm version minor && git push && git push --tags",
|
|
303
|
-
"release:major": "npm version major && git push && git push --tags"
|
|
304
|
-
},
|
|
305
|
-
keywords: [
|
|
306
|
-
"secrets",
|
|
307
|
-
"env",
|
|
308
|
-
"keyway",
|
|
309
|
-
"cli",
|
|
310
|
-
"devops"
|
|
311
|
-
],
|
|
312
|
-
author: "Nicolas Ritouet",
|
|
313
|
-
license: "MIT",
|
|
314
|
-
homepage: "https://keyway.sh",
|
|
315
|
-
repository: {
|
|
316
|
-
type: "git",
|
|
317
|
-
url: "https://github.com/keywaysh/cli.git"
|
|
318
|
-
},
|
|
319
|
-
bugs: {
|
|
320
|
-
url: "https://github.com/keywaysh/cli/issues"
|
|
321
|
-
},
|
|
322
|
-
packageManager: "pnpm@10.6.1",
|
|
323
|
-
engines: {
|
|
324
|
-
node: ">=18.0.0"
|
|
325
|
-
},
|
|
326
|
-
dependencies: {
|
|
327
|
-
chalk: "^4.1.2",
|
|
328
|
-
commander: "^14.0.0",
|
|
329
|
-
conf: "^15.0.2",
|
|
330
|
-
open: "^11.0.0",
|
|
331
|
-
"posthog-node": "^3.5.0",
|
|
332
|
-
prompts: "^2.4.2"
|
|
333
|
-
},
|
|
334
|
-
devDependencies: {
|
|
335
|
-
"@types/node": "^24.2.0",
|
|
336
|
-
"@types/prompts": "^2.4.9",
|
|
337
|
-
tsup: "^8.5.0",
|
|
338
|
-
tsx: "^4.20.3",
|
|
339
|
-
typescript: "^5.9.2",
|
|
340
|
-
vitest: "^3.2.4"
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
// src/utils/analytics.ts
|
|
345
302
|
var posthog = null;
|
|
346
303
|
var distinctId = null;
|
|
347
304
|
var CONFIG_DIR = path.join(os.homedir(), ".config", "keyway");
|
|
@@ -398,7 +355,7 @@ function trackEvent(event, properties) {
|
|
|
398
355
|
source: "cli",
|
|
399
356
|
platform: process.platform,
|
|
400
357
|
nodeVersion: process.version,
|
|
401
|
-
version:
|
|
358
|
+
version: package_default.version,
|
|
402
359
|
ci: CI
|
|
403
360
|
}
|
|
404
361
|
});
|
|
@@ -442,33 +399,86 @@ import prompts from "prompts";
|
|
|
442
399
|
|
|
443
400
|
// src/utils/auth.ts
|
|
444
401
|
import Conf from "conf";
|
|
402
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
|
|
403
|
+
import { promisify } from "util";
|
|
445
404
|
var store = new Conf({
|
|
446
405
|
projectName: "keyway",
|
|
447
406
|
configName: "config",
|
|
448
407
|
fileMode: 384
|
|
449
408
|
});
|
|
409
|
+
var scryptAsync = promisify(scrypt);
|
|
410
|
+
async function getEncryptionKey() {
|
|
411
|
+
const machineId = process.env.USER || process.env.USERNAME || "keyway-user";
|
|
412
|
+
let salt = store.get("salt");
|
|
413
|
+
if (!salt) {
|
|
414
|
+
salt = randomBytes(16).toString("hex");
|
|
415
|
+
store.set("salt", salt);
|
|
416
|
+
}
|
|
417
|
+
const key = await scryptAsync(machineId, salt, 32);
|
|
418
|
+
return key;
|
|
419
|
+
}
|
|
420
|
+
async function encryptToken(token) {
|
|
421
|
+
const key = await getEncryptionKey();
|
|
422
|
+
const iv = randomBytes(16);
|
|
423
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
424
|
+
const encrypted = Buffer.concat([
|
|
425
|
+
cipher.update(token, "utf8"),
|
|
426
|
+
cipher.final()
|
|
427
|
+
]);
|
|
428
|
+
const authTag = cipher.getAuthTag();
|
|
429
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
430
|
+
}
|
|
431
|
+
async function decryptToken(encryptedData) {
|
|
432
|
+
const key = await getEncryptionKey();
|
|
433
|
+
const parts = encryptedData.split(":");
|
|
434
|
+
if (parts.length !== 3) {
|
|
435
|
+
throw new Error("Invalid encrypted token format");
|
|
436
|
+
}
|
|
437
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
438
|
+
const authTag = Buffer.from(parts[1], "hex");
|
|
439
|
+
const encrypted = Buffer.from(parts[2], "hex");
|
|
440
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
441
|
+
decipher.setAuthTag(authTag);
|
|
442
|
+
const decrypted = Buffer.concat([
|
|
443
|
+
decipher.update(encrypted),
|
|
444
|
+
decipher.final()
|
|
445
|
+
]);
|
|
446
|
+
return decrypted.toString("utf8");
|
|
447
|
+
}
|
|
450
448
|
function isExpired(auth) {
|
|
451
449
|
if (!auth.expiresAt) return false;
|
|
452
450
|
const expires = Date.parse(auth.expiresAt);
|
|
453
451
|
if (Number.isNaN(expires)) return false;
|
|
454
452
|
return expires <= Date.now();
|
|
455
453
|
}
|
|
456
|
-
function getStoredAuth() {
|
|
457
|
-
const
|
|
458
|
-
if (
|
|
454
|
+
async function getStoredAuth() {
|
|
455
|
+
const encryptedData = store.get("auth");
|
|
456
|
+
if (!encryptedData) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const decrypted = await decryptToken(encryptedData);
|
|
461
|
+
const auth = JSON.parse(decrypted);
|
|
462
|
+
if (isExpired(auth)) {
|
|
463
|
+
clearAuth();
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
return auth;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error("Failed to decrypt stored auth, clearing...");
|
|
459
469
|
clearAuth();
|
|
460
470
|
return null;
|
|
461
471
|
}
|
|
462
|
-
return auth ?? null;
|
|
463
472
|
}
|
|
464
|
-
function saveAuthToken(token, meta) {
|
|
473
|
+
async function saveAuthToken(token, meta) {
|
|
465
474
|
const auth = {
|
|
466
475
|
keywayToken: token,
|
|
467
476
|
githubLogin: meta?.githubLogin,
|
|
468
477
|
expiresAt: meta?.expiresAt,
|
|
469
478
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
470
479
|
};
|
|
471
|
-
|
|
480
|
+
const encrypted = await encryptToken(JSON.stringify(auth));
|
|
481
|
+
store.set("auth", encrypted);
|
|
472
482
|
}
|
|
473
483
|
function clearAuth() {
|
|
474
484
|
store.delete("auth");
|
|
@@ -526,7 +536,7 @@ async function runLoginFlow() {
|
|
|
526
536
|
continue;
|
|
527
537
|
}
|
|
528
538
|
if (result.status === "approved" && result.keywayToken) {
|
|
529
|
-
saveAuthToken(result.keywayToken, {
|
|
539
|
+
await saveAuthToken(result.keywayToken, {
|
|
530
540
|
githubLogin: result.githubLogin,
|
|
531
541
|
expiresAt: result.expiresAt
|
|
532
542
|
});
|
|
@@ -548,7 +558,7 @@ async function ensureLogin(options = {}) {
|
|
|
548
558
|
if (envToken) {
|
|
549
559
|
return envToken;
|
|
550
560
|
}
|
|
551
|
-
const stored = getStoredAuth();
|
|
561
|
+
const stored = await getStoredAuth();
|
|
552
562
|
if (stored?.keywayToken) {
|
|
553
563
|
return stored.keywayToken;
|
|
554
564
|
}
|
|
@@ -601,7 +611,7 @@ async function runTokenLogin() {
|
|
|
601
611
|
throw new Error("Token must start with github_pat_.");
|
|
602
612
|
}
|
|
603
613
|
const validation = await validateToken(trimmedToken);
|
|
604
|
-
saveAuthToken(trimmedToken, {
|
|
614
|
+
await saveAuthToken(trimmedToken, {
|
|
605
615
|
githubLogin: validation.username
|
|
606
616
|
});
|
|
607
617
|
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
@@ -994,6 +1004,22 @@ async function initCommand(options = {}) {
|
|
|
994
1004
|
await shutdownAnalytics();
|
|
995
1005
|
return;
|
|
996
1006
|
}
|
|
1007
|
+
if (error.error === "PLAN_LIMIT_REACHED") {
|
|
1008
|
+
console.log("");
|
|
1009
|
+
console.log(chalk4.dim("\u2500".repeat(50)));
|
|
1010
|
+
console.log("");
|
|
1011
|
+
console.log(` ${chalk4.yellow("\u26A1")} ${chalk4.bold("Upgrade to Pro")}`);
|
|
1012
|
+
console.log("");
|
|
1013
|
+
console.log(chalk4.gray(" You've reached the limit of your free plan."));
|
|
1014
|
+
console.log(chalk4.gray(" Upgrade to Pro for unlimited private repositories."));
|
|
1015
|
+
console.log("");
|
|
1016
|
+
console.log(` ${chalk4.cyan("\u2192")} ${chalk4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log(chalk4.dim("\u2500".repeat(50)));
|
|
1019
|
+
console.log("");
|
|
1020
|
+
await shutdownAnalytics();
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
997
1023
|
}
|
|
998
1024
|
const message = error instanceof APIError ? error.message : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
|
|
999
1025
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
@@ -1262,7 +1288,7 @@ async function checkSystemClock() {
|
|
|
1262
1288
|
try {
|
|
1263
1289
|
const controller = new AbortController();
|
|
1264
1290
|
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1265
|
-
const response = await fetch(
|
|
1291
|
+
const response = await fetch(API_HEALTH_URL, {
|
|
1266
1292
|
method: "HEAD",
|
|
1267
1293
|
signal: controller.signal
|
|
1268
1294
|
});
|