@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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/cli.js +86 -78
  3. 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
- More details: [keyway.sh/security](https://keyway.sh/security)
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**: unlock@keyway.sh
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 with { type: 'json' }
69
+ // package.json
70
70
  var package_default = {
71
71
  name: "@keywaysh/cli",
72
- version: "0.0.13",
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: package_default2.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 auth = store.get("auth");
460
- if (auth && isExpired(auth)) {
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
- store.set("auth", auth);
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("https://api.keyway.sh/v1/health", {
1291
+ const response = await fetch(API_HEALTH_URL, {
1284
1292
  method: "HEAD",
1285
1293
  signal: controller.signal
1286
1294
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keywaysh/cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {