@bifos/nhncloud-cli 0.10.0 → 0.10.1

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 -0
  2. package/dist/index.js +39 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # nhncloud-cli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@bifos/nhncloud-cli.svg)](https://www.npmjs.com/package/@bifos/nhncloud-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@bifos/nhncloud-cli.svg)](https://www.npmjs.com/package/@bifos/nhncloud-cli)
5
+ [![CI](https://github.com/jon890/nhncloud-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/jon890/nhncloud-cli/actions/workflows/ci.yml)
6
+ [![license](https://img.shields.io/npm/l/@bifos/nhncloud-cli.svg)](https://github.com/jon890/nhncloud-cli/blob/main/LICENSE)
7
+
3
8
  NHN Cloud 서비스를 AWS CLI 방식으로 호출하는 통합 CLI.
4
9
  현재 106개 command catalog 항목을 지원한다.
5
10
  `configure`, `commands`, `logncrash search/send/export`, `deploy`, `instance`, `network`, `volume`, `floatingip`, `ncr`, `nks`, `ncs` 명령으로 NHN Cloud 서비스를 조회·운영할 수 있다.
package/dist/index.js CHANGED
@@ -315,15 +315,18 @@ var import_node_path2 = require("path");
315
315
  var import_node_os2 = require("os");
316
316
  var import_node_crypto = require("crypto");
317
317
  var CACHE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".nhncloud", "cache");
318
+ function credentialFingerprint(material) {
319
+ return (0, import_node_crypto.createHash)("sha256").update(material).digest("hex");
320
+ }
318
321
  function iaasCachePath(profile, region) {
319
322
  return (0, import_node_path2.join)(CACHE_DIR, `iaas-token-${profile}-${region}.json`);
320
323
  }
321
324
  function isIaasTokenCache(val) {
322
325
  if (typeof val !== "object" || val === null) return false;
323
326
  const obj = val;
324
- return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string" && typeof obj["imageEndpoint"] === "string" && typeof obj["networkEndpoint"] === "string" && typeof obj["blockStorageEndpoint"] === "string" && typeof obj["nksEndpoint"] === "string";
327
+ return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["credentialHash"] === "string" && typeof obj["computeEndpoint"] === "string" && typeof obj["imageEndpoint"] === "string" && typeof obj["networkEndpoint"] === "string" && typeof obj["blockStorageEndpoint"] === "string" && typeof obj["nksEndpoint"] === "string";
325
328
  }
326
- async function readIaasToken(profile, region) {
329
+ async function readIaasToken(profile, region, credentialHash) {
327
330
  const filePath = iaasCachePath(profile, region);
328
331
  try {
329
332
  const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
@@ -332,6 +335,7 @@ async function readIaasToken(profile, region) {
332
335
  const expiresAt = new Date(parsed.expiresAt).getTime();
333
336
  const BUFFER_MS = 6e4;
334
337
  if (expiresAt - Date.now() < BUFFER_MS) return null;
338
+ if (parsed.credentialHash !== credentialHash) return null;
335
339
  return {
336
340
  tokenId: parsed.tokenId,
337
341
  expiresAt: parsed.expiresAt,
@@ -351,6 +355,7 @@ async function writeIaasToken(profile, region, data) {
351
355
  const cache = {
352
356
  tokenId: data.tokenId,
353
357
  expiresAt: data.expiresAt,
358
+ credentialHash: data.credentialHash,
354
359
  computeEndpoint: data.computeEndpoint,
355
360
  imageEndpoint: data.imageEndpoint,
356
361
  networkEndpoint: data.networkEndpoint,
@@ -362,14 +367,14 @@ async function writeIaasToken(profile, region, data) {
362
367
  await (0, import_promises2.rename)(tmp, filePath);
363
368
  }
364
369
  function tokenCachePath(profile) {
365
- return (0, import_node_path2.join)(CACHE_DIR, `deploy-token-${profile}.json`);
370
+ return (0, import_node_path2.join)(CACHE_DIR, `user-access-token-${profile}.json`);
366
371
  }
367
372
  function isTokenCache(val) {
368
373
  if (typeof val !== "object" || val === null) return false;
369
374
  const obj = val;
370
- return typeof obj["accessToken"] === "string" && typeof obj["expiresAt"] === "string";
375
+ return typeof obj["accessToken"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["credentialHash"] === "string";
371
376
  }
372
- async function readToken(profile) {
377
+ async function readToken(profile, credentialHash) {
373
378
  const filePath = tokenCachePath(profile);
374
379
  try {
375
380
  const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
@@ -379,17 +384,19 @@ async function readToken(profile) {
379
384
  const now = Date.now();
380
385
  const BUFFER_MS = 6e4;
381
386
  if (expiresAt - now < BUFFER_MS) return null;
387
+ if (parsed.credentialHash !== credentialHash) return null;
382
388
  return { accessToken: parsed.accessToken, expiresAt: parsed.expiresAt };
383
389
  } catch {
384
390
  return null;
385
391
  }
386
392
  }
387
- async function writeToken(profile, accessToken, expiresAt) {
393
+ async function writeToken(profile, accessToken, expiresAt, credentialHash) {
388
394
  const filePath = tokenCachePath(profile);
389
395
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true });
390
396
  const data = {
391
397
  accessToken,
392
- expiresAt: expiresAt.toISOString()
398
+ expiresAt: expiresAt.toISOString(),
399
+ credentialHash
393
400
  };
394
401
  const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(4).toString("hex") + ".tmp";
395
402
  await (0, import_promises2.writeFile)(tmp, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
@@ -424,8 +431,9 @@ function isTokenResponse(val) {
424
431
  return typeof obj["access_token"] === "string" && typeof obj["expires_in"] === "number";
425
432
  }
426
433
  async function getAccessToken(profile, uakId, uakSecret, forceRefresh = false) {
434
+ const credentialHash = credentialFingerprint(JSON.stringify([uakId, uakSecret]));
427
435
  if (!forceRefresh) {
428
- const cached = await readToken(profile);
436
+ const cached = await readToken(profile, credentialHash);
429
437
  if (cached !== null) {
430
438
  return cached.accessToken;
431
439
  }
@@ -449,7 +457,7 @@ async function getAccessToken(profile, uakId, uakSecret, forceRefresh = false) {
449
457
  }
450
458
  if (!forceRefresh) {
451
459
  const expiresAt = new Date(Date.now() + raw.expires_in * 1e3);
452
- await writeToken(profile, raw.access_token, expiresAt);
460
+ await writeToken(profile, raw.access_token, expiresAt, credentialHash);
453
461
  }
454
462
  return raw.access_token;
455
463
  }
@@ -597,8 +605,11 @@ function isKeystoneTokenResponse(val) {
597
605
  return typeof token["id"] === "string" && typeof token["expires"] === "string";
598
606
  }
599
607
  async function getIaasToken(profile, iaas, forceRefresh = false) {
608
+ const credentialHash = credentialFingerprint(
609
+ JSON.stringify([iaas.tenantId, iaas.username, iaas.password])
610
+ );
600
611
  if (!forceRefresh) {
601
- const cached = await readIaasToken(profile, iaas.region);
612
+ const cached = await readIaasToken(profile, iaas.region, credentialHash);
602
613
  if (cached !== null) {
603
614
  return {
604
615
  tokenId: cached.tokenId,
@@ -642,7 +653,7 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
642
653
  const tokenId = raw.access.token.id;
643
654
  const expiresAt = raw.access.token.expires;
644
655
  if (!forceRefresh) {
645
- await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint });
656
+ await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, credentialHash, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint });
646
657
  }
647
658
  return { tokenId, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint, nksEndpoint };
648
659
  }
@@ -3887,11 +3898,6 @@ function isVolume(val) {
3887
3898
  return typeof obj["id"] === "string" && // Cinder 는 --name 미지정 시 null — null 인 볼륨 하나가 list 전체를 거부하지 않게 허용 (isImage 선례).
3888
3899
  (typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["size"] === "number" && typeof obj["status"] === "string" && (obj["availability_zone"] === void 0 || typeof obj["availability_zone"] === "string") && Array.isArray(obj["attachments"]);
3889
3900
  }
3890
- function isVolumesResponse(val) {
3891
- if (typeof val !== "object" || val === null) return false;
3892
- const obj = val;
3893
- return Array.isArray(obj["volumes"]) && obj["volumes"].every(isVolume);
3894
- }
3895
3901
  function isVolumeResponse(val) {
3896
3902
  if (typeof val !== "object" || val === null) return false;
3897
3903
  const obj = val;
@@ -3909,7 +3915,7 @@ var BlockStorageClient = class {
3909
3915
  return { "X-Auth-Token": this.tokenId };
3910
3916
  }
3911
3917
  async list(params) {
3912
- const url = `${this.endpoint}/volumes`;
3918
+ const url = `${this.endpoint}/volumes/detail`;
3913
3919
  const searchParams = {};
3914
3920
  if (params?.sort !== void 0) searchParams["sort"] = params.sort;
3915
3921
  if (params?.limit !== void 0) searchParams["limit"] = params.limit;
@@ -3917,13 +3923,26 @@ var BlockStorageClient = class {
3917
3923
  if (params?.marker !== void 0) searchParams["marker"] = params.marker;
3918
3924
  try {
3919
3925
  const raw = await import_ky9.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS5 }).json();
3920
- if (!isVolumesResponse(raw)) {
3926
+ if (typeof raw !== "object" || raw === null) {
3921
3927
  throw new NhnCloudCliError(
3922
3928
  "volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
3923
3929
  EXIT_API_ERROR
3924
3930
  );
3925
3931
  }
3926
- return raw.volumes;
3932
+ const obj = raw;
3933
+ if (!Array.isArray(obj["volumes"])) {
3934
+ throw new NhnCloudCliError(
3935
+ "volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
3936
+ EXIT_API_ERROR
3937
+ );
3938
+ }
3939
+ if (!obj["volumes"].every(isVolume)) {
3940
+ throw new NhnCloudCliError(
3941
+ "volume list \uC751\uB2F5\uC758 \uBCFC\uB968 \uD56D\uBAA9 \uD615\uC2DD\uC774 \uC608\uC0C1\uACFC \uB2E4\uB985\uB2C8\uB2E4 \u2014 API \uC751\uB2F5 \uD544\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
3942
+ EXIT_API_ERROR
3943
+ );
3944
+ }
3945
+ return obj["volumes"];
3927
3946
  } catch (err) {
3928
3947
  throw toNhnCloudCliError(err);
3929
3948
  }
@@ -7592,7 +7611,7 @@ Agent workflow:
7592
7611
  3. nhncloud ncs workload get <id> --json
7593
7612
  `;
7594
7613
  var program = new import_commander48.Command();
7595
- program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.10.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654").addHelpText("after", rootAgentHints);
7614
+ program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.10.1").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654").addHelpText("after", rootAgentHints);
7596
7615
  program.hook("preAction", () => {
7597
7616
  const opts = program.opts();
7598
7617
  if (!opts.color || process.env["NO_COLOR"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifos/nhncloud-cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI tool for NHN Cloud services — AI agent & terminal friendly",
5
5
  "keywords": [
6
6
  "nhncloud",