@keywaysh/cli 0.0.13 → 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 +86 -78
- 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,6 +132,26 @@ 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
156
|
constructor(statusCode, error, message, upgradeUrl) {
|
|
137
157
|
super(message);
|
|
@@ -279,71 +299,6 @@ import crypto from "crypto";
|
|
|
279
299
|
import path from "path";
|
|
280
300
|
import os from "os";
|
|
281
301
|
import fs from "fs";
|
|
282
|
-
|
|
283
|
-
// package.json
|
|
284
|
-
var package_default2 = {
|
|
285
|
-
name: "@keywaysh/cli",
|
|
286
|
-
version: "0.0.13",
|
|
287
|
-
description: "One link to all your secrets",
|
|
288
|
-
type: "module",
|
|
289
|
-
bin: {
|
|
290
|
-
keyway: "./dist/cli.js"
|
|
291
|
-
},
|
|
292
|
-
main: "./dist/cli.js",
|
|
293
|
-
files: [
|
|
294
|
-
"dist"
|
|
295
|
-
],
|
|
296
|
-
scripts: {
|
|
297
|
-
dev: "pnpm exec tsx src/cli.ts",
|
|
298
|
-
build: "pnpm exec tsup",
|
|
299
|
-
"build:watch": "pnpm exec tsup --watch",
|
|
300
|
-
prepublishOnly: "pnpm run build",
|
|
301
|
-
test: "pnpm exec vitest run",
|
|
302
|
-
"test:watch": "pnpm exec vitest",
|
|
303
|
-
release: "npm version patch && git push && git push --tags",
|
|
304
|
-
"release:minor": "npm version minor && git push && git push --tags",
|
|
305
|
-
"release:major": "npm version major && git push && git push --tags"
|
|
306
|
-
},
|
|
307
|
-
keywords: [
|
|
308
|
-
"secrets",
|
|
309
|
-
"env",
|
|
310
|
-
"keyway",
|
|
311
|
-
"cli",
|
|
312
|
-
"devops"
|
|
313
|
-
],
|
|
314
|
-
author: "Nicolas Ritouet",
|
|
315
|
-
license: "MIT",
|
|
316
|
-
homepage: "https://keyway.sh",
|
|
317
|
-
repository: {
|
|
318
|
-
type: "git",
|
|
319
|
-
url: "https://github.com/keywaysh/cli.git"
|
|
320
|
-
},
|
|
321
|
-
bugs: {
|
|
322
|
-
url: "https://github.com/keywaysh/cli/issues"
|
|
323
|
-
},
|
|
324
|
-
packageManager: "pnpm@10.6.1",
|
|
325
|
-
engines: {
|
|
326
|
-
node: ">=18.0.0"
|
|
327
|
-
},
|
|
328
|
-
dependencies: {
|
|
329
|
-
chalk: "^4.1.2",
|
|
330
|
-
commander: "^14.0.0",
|
|
331
|
-
conf: "^15.0.2",
|
|
332
|
-
open: "^11.0.0",
|
|
333
|
-
"posthog-node": "^3.5.0",
|
|
334
|
-
prompts: "^2.4.2"
|
|
335
|
-
},
|
|
336
|
-
devDependencies: {
|
|
337
|
-
"@types/node": "^24.2.0",
|
|
338
|
-
"@types/prompts": "^2.4.9",
|
|
339
|
-
tsup: "^8.5.0",
|
|
340
|
-
tsx: "^4.20.3",
|
|
341
|
-
typescript: "^5.9.2",
|
|
342
|
-
vitest: "^3.2.4"
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
// src/utils/analytics.ts
|
|
347
302
|
var posthog = null;
|
|
348
303
|
var distinctId = null;
|
|
349
304
|
var CONFIG_DIR = path.join(os.homedir(), ".config", "keyway");
|
|
@@ -400,7 +355,7 @@ function trackEvent(event, properties) {
|
|
|
400
355
|
source: "cli",
|
|
401
356
|
platform: process.platform,
|
|
402
357
|
nodeVersion: process.version,
|
|
403
|
-
version:
|
|
358
|
+
version: package_default.version,
|
|
404
359
|
ci: CI
|
|
405
360
|
}
|
|
406
361
|
});
|
|
@@ -444,33 +399,86 @@ import prompts from "prompts";
|
|
|
444
399
|
|
|
445
400
|
// src/utils/auth.ts
|
|
446
401
|
import Conf from "conf";
|
|
402
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
|
|
403
|
+
import { promisify } from "util";
|
|
447
404
|
var store = new Conf({
|
|
448
405
|
projectName: "keyway",
|
|
449
406
|
configName: "config",
|
|
450
407
|
fileMode: 384
|
|
451
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
|
+
}
|
|
452
448
|
function isExpired(auth) {
|
|
453
449
|
if (!auth.expiresAt) return false;
|
|
454
450
|
const expires = Date.parse(auth.expiresAt);
|
|
455
451
|
if (Number.isNaN(expires)) return false;
|
|
456
452
|
return expires <= Date.now();
|
|
457
453
|
}
|
|
458
|
-
function getStoredAuth() {
|
|
459
|
-
const
|
|
460
|
-
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...");
|
|
461
469
|
clearAuth();
|
|
462
470
|
return null;
|
|
463
471
|
}
|
|
464
|
-
return auth ?? null;
|
|
465
472
|
}
|
|
466
|
-
function saveAuthToken(token, meta) {
|
|
473
|
+
async function saveAuthToken(token, meta) {
|
|
467
474
|
const auth = {
|
|
468
475
|
keywayToken: token,
|
|
469
476
|
githubLogin: meta?.githubLogin,
|
|
470
477
|
expiresAt: meta?.expiresAt,
|
|
471
478
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
472
479
|
};
|
|
473
|
-
|
|
480
|
+
const encrypted = await encryptToken(JSON.stringify(auth));
|
|
481
|
+
store.set("auth", encrypted);
|
|
474
482
|
}
|
|
475
483
|
function clearAuth() {
|
|
476
484
|
store.delete("auth");
|
|
@@ -528,7 +536,7 @@ async function runLoginFlow() {
|
|
|
528
536
|
continue;
|
|
529
537
|
}
|
|
530
538
|
if (result.status === "approved" && result.keywayToken) {
|
|
531
|
-
saveAuthToken(result.keywayToken, {
|
|
539
|
+
await saveAuthToken(result.keywayToken, {
|
|
532
540
|
githubLogin: result.githubLogin,
|
|
533
541
|
expiresAt: result.expiresAt
|
|
534
542
|
});
|
|
@@ -550,7 +558,7 @@ async function ensureLogin(options = {}) {
|
|
|
550
558
|
if (envToken) {
|
|
551
559
|
return envToken;
|
|
552
560
|
}
|
|
553
|
-
const stored = getStoredAuth();
|
|
561
|
+
const stored = await getStoredAuth();
|
|
554
562
|
if (stored?.keywayToken) {
|
|
555
563
|
return stored.keywayToken;
|
|
556
564
|
}
|
|
@@ -603,7 +611,7 @@ async function runTokenLogin() {
|
|
|
603
611
|
throw new Error("Token must start with github_pat_.");
|
|
604
612
|
}
|
|
605
613
|
const validation = await validateToken(trimmedToken);
|
|
606
|
-
saveAuthToken(trimmedToken, {
|
|
614
|
+
await saveAuthToken(trimmedToken, {
|
|
607
615
|
githubLogin: validation.username
|
|
608
616
|
});
|
|
609
617
|
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
@@ -1280,7 +1288,7 @@ async function checkSystemClock() {
|
|
|
1280
1288
|
try {
|
|
1281
1289
|
const controller = new AbortController();
|
|
1282
1290
|
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1283
|
-
const response = await fetch(
|
|
1291
|
+
const response = await fetch(API_HEALTH_URL, {
|
|
1284
1292
|
method: "HEAD",
|
|
1285
1293
|
signal: controller.signal
|
|
1286
1294
|
});
|