@bstage-sdk/cli 0.1.0 → 0.3.0

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/dist/MIGRATION.md CHANGED
@@ -45,6 +45,35 @@
45
45
 
46
46
  > **`적용` 단계는 옛 프로젝트에서 실제로 따라가 본 뒤 적는다.** 새 `bstage init` 프로젝트에서 되는 것과, 기존 프로젝트가 이 절차만 보고 도달하는 것은 다르다. 따라 했을 때 깨지는 선행 조건이 있으면 **그 조건까지 적는다** — `core 0.44.0` 항목이 그 사례다(스크립트만 넣으면 `@types/node` 부재로 `TS2688`이 난다. 스캐폴드 템플릿을 고쳐 새 프로젝트는 해결됐지만, 먼저 써둔 적용 절차를 되짚지 않아 기존 프로젝트용 지침이 깨진 채 남았다).
47
47
 
48
- > **헤더 규칙**: `
48
+ > **헤더 규칙**: `## → {패키지} {버전}` 형식으로, 그 변경을 실제로 낸 패키지와 버전을 명시한다(예: `→ cli 0.3.0`, `→ core 0.3.0`). 한 변경이 여러 패키지를 함께 범프시켰으면 소비자가 올려야 할 패키지를 `·`로 나열한다(예: `→ core 0.3.0 · cli 0.3.0`). 소비자는 이 헤더의 패키지를 해당 버전 **이상**으로 올려야 한다. 단순 의존성 재핀(dependency bump)만 된 패키지는 헤더에 적지 않는다.
49
+ >
50
+ > **정렬**: 릴리즈 **시점 최신이 위**. 패키지가 독립 버전이라 헤더 숫자만으로는 시간순 정렬이 되지 않으므로 릴리즈 순서를 기준으로 한다.
49
51
 
50
- 아직 항목이 없다. 소비자에게 영향이 있는 변경을 릴리즈할 때 여기에 추가한다.
52
+ ---
53
+ ## → core 0.3.0 · cli 0.3.0
54
+
55
+ ### 앱 키 이름 정리(`appSecret` → `appKey`, `VITE_BSTAGE_APP_SECRET` → `VITE_BSTAGE_APP_KEY`) — `판단` · `선택`
56
+
57
+ **영향**
58
+
59
+ - **기존 프로젝트가 반드시 할 일은 없다.** `appSecret`·`VITE_BSTAGE_APP_SECRET`은 deprecated 별칭으로 그대로 동작한다. 별칭은 **0.4.0**에서 제거되므로 그 전에 옮긴다.
60
+ - 감지: `src/shared/client.ts`에 `appSecret:`이 있거나, `.env`·`.env.example`에 `VITE_BSTAGE_APP_SECRET`이 있는 경우.
61
+
62
+ **변경 내용**
63
+
64
+ - 같은 값을 포털 화면(APP KEY)·포털 API(`appKey`)·게이트웨이 헤더(`X-BSTAGE-APP-KEY`)는 모두 KEY라고 부르는데 SDK만 `secret`이라고 불렀다. 이 값은 `VITE_` 환경변수라 **빌드 결과물에 평문으로 실려 브라우저에서 보이는 값**이고 비밀값이 아니다 — 이름이 사실과 반대여서 "숨겨야 하는 값"으로 오해하게 했다. SDK 이름을 나머지와 맞췄다.
65
+ - `BstageConfig.appKey`가 정본이고 `appSecret`은 deprecated 별칭이다(둘 다 주면 `appKey` 우선). 타입 주석도 "앱 시크릿"에서 "앱 키(APP KEY) — 브라우저에 노출되는 값"으로 바꿨다.
66
+ - 새 `bstage init` 스캐폴드는 `.env`·`.env.example`·`client.ts`·`vite-env.d.ts`에 새 이름을 쓴다.
67
+ - **소스와 `.env`가 다른 이름을 써도 빌드가 이어 붙인다.** `bstage build`(프리셋)와 `bstageDevPlugin`이 `VITE_BSTAGE_APP_KEY`·`VITE_BSTAGE_APP_SECRET` 중 한쪽만 있으면 다른 이름으로도 같은 값이 읽히게 `define`한다. 그래서 옛 `client.ts`(`appSecret: import.meta.env.VITE_BSTAGE_APP_SECRET`)를 둔 채 `.env`만 새 이름으로 바꿔도, 포털의 환경변수 입력이 새 이름으로 들어와도 번들에 값이 실린다. 둘 다 있으면 각자 실제 값이 그대로 쓰인다.
68
+ - `bstage build`의 인증 값 점검도 같은 순서로 본다 — 소스가 읽는 이름의 값이 우선, 다른 이름은 폴백. 보고에는 소스가 실제로 읽는 이름을 쓴다.
69
+ - 앱 키가 아예 없으면 `X-BSTAGE-APP-KEY` 헤더에 `"undefined"` 문자열을 실던 것을 헤더 생략 + 생성 시 콘솔 경고로 바꿨다. 게이트웨이 응답(401)은 같다.
70
+
71
+ **적용**
72
+
73
+ 1. (자동) `@bstage-sdk/core`·`@bstage-sdk/cli`를 `0.3.0` 이상으로 올린다.
74
+ 2. (판단) `src/shared/client.ts`의 `appSecret: import.meta.env.VITE_BSTAGE_APP_SECRET`을 `appKey: import.meta.env.VITE_BSTAGE_APP_KEY`로 바꾼다. 리터럴 키를 박아 둔 옛 프로젝트는 키 이름만 `appSecret` → `appKey`로 바꾼다.
75
+ 3. (판단) `.env`·`.env.example`의 `VITE_BSTAGE_APP_SECRET=`을 `VITE_BSTAGE_APP_KEY=`로 바꾼다. 값은 그대로다 — 재발급이 아니다.
76
+ 4. (판단) `src/vite-env.d.ts`의 `readonly VITE_BSTAGE_APP_SECRET: string`을 `readonly VITE_BSTAGE_APP_KEY: string`으로 바꾼다.
77
+ 5. CI·배포 환경에 `VITE_BSTAGE_APP_SECRET`을 주입하고 있었다면 같은 값을 `VITE_BSTAGE_APP_KEY`로 주입한다. 2~4를 한 번에 못 옮기는 기간에는 위 별칭 덕에 어느 한쪽 이름만 있어도 빌드가 통과한다.
78
+
79
+ ---
@@ -2,14 +2,14 @@
2
2
  import { defineConfig } from "vite";
3
3
  import react from "@vitejs/plugin-react";
4
4
 
5
- // src/dev/devVitePlugin.ts
6
- import { createRequire } from "module";
7
- import { readFileSync as readFileSync2 } from "fs";
8
- import { join, dirname } from "path";
5
+ // src/vite/appKeyEnvAlias.ts
6
+ import { loadEnv } from "vite";
9
7
 
10
8
  // src/constants.ts
11
9
  import { readFileSync } from "fs";
12
10
  import { resolve } from "path";
11
+ var APP_KEY_VAR = "VITE_BSTAGE_APP_KEY";
12
+ var LEGACY_APP_KEY_VAR = "VITE_BSTAGE_APP_SECRET";
13
13
  var BSTAGE_INTERNAL_PREFIX = "/__bstage__";
14
14
  var LOGIN_PATH = `${BSTAGE_INTERNAL_PREFIX}/login`;
15
15
  var PROXY_PATH_PREFIX = "/__proxy__";
@@ -65,6 +65,11 @@ var DEFAULT_TRANSLATION_TIER = "inhouse";
65
65
  var TRANSLATION_CDN_ORIGIN = "https://cdn.static.bstage.in";
66
66
  var TRANSLATION_CDN_BASE_KEY = "static/i18n";
67
67
  var TRANSLATION_CDN_BUCKET_PREFIX = "cdn.bstage.in/";
68
+ var PORTAL_HOSTS = {
69
+ real: "portal.bstage.in",
70
+ sandbox: "bstage-portal.sandbox.bstage.systems"
71
+ };
72
+ var PORTAL_HOST_ENV_KEY = "VITE_BSTAGE_PORTAL_HOST";
68
73
  function getAuthHost(mode, phase, tenantId) {
69
74
  if (mode === "admin") {
70
75
  const fn2 = ADMIN_AUTH_HOSTS[phase];
@@ -83,6 +88,31 @@ function resolveProjectTarget(root) {
83
88
  return pkg.bstage?.target === "admin" ? "admin" : "user";
84
89
  }
85
90
 
91
+ // src/vite/appKeyEnvAlias.ts
92
+ function appKeyEnvDefine(env) {
93
+ const key = env[APP_KEY_VAR]?.trim();
94
+ const legacy = env[LEGACY_APP_KEY_VAR]?.trim();
95
+ if (key && !legacy) return { [`import.meta.env.${LEGACY_APP_KEY_VAR}`]: JSON.stringify(key) };
96
+ if (legacy && !key) return { [`import.meta.env.${APP_KEY_VAR}`]: JSON.stringify(legacy) };
97
+ return {};
98
+ }
99
+ function loadProjectEnv(config, mode) {
100
+ return loadEnv(mode, config.envDir ?? config.root ?? process.cwd(), "VITE_");
101
+ }
102
+ function appKeyEnvAliasPlugin() {
103
+ return {
104
+ name: "bstage-app-key-env-alias",
105
+ config(config, { mode }) {
106
+ return { define: appKeyEnvDefine(loadProjectEnv(config, mode)) };
107
+ }
108
+ };
109
+ }
110
+
111
+ // src/dev/devVitePlugin.ts
112
+ import { createRequire } from "module";
113
+ import { readFileSync as readFileSync2 } from "fs";
114
+ import { join, dirname } from "path";
115
+
86
116
  // src/dev/loginPage.ts
87
117
  function getLoginPageHtml(phase, mode = "user") {
88
118
  const tokenPath = AUTH_TOKEN_PATH_BY_MODE[mode].replace(/^\/svc/, AUTH_PATH_PREFIX);
@@ -410,14 +440,14 @@ function getLoginPageHtml(phase, mode = "user") {
410
440
  // src/dev/proxyUtils.ts
411
441
  import { request as httpsRequest } from "https";
412
442
  import { gunzipSync, brotliDecompressSync, inflateSync } from "zlib";
413
- import { loadEnv } from "vite";
443
+ import { loadEnv as loadEnv2 } from "vite";
414
444
  var CF_ACCESS_ENV_KEYS = ["VITE_CF_ACCESS_CLIENT_ID", "VITE_CF_ACCESS_CLIENT_SECRET"];
415
445
  var cachedCreds;
416
446
  var missingWarned = false;
417
447
  function resolveCfAccessCreds() {
418
448
  if (cachedCreds !== void 0) return cachedCreds;
419
449
  const [idKey, secretKey] = CF_ACCESS_ENV_KEYS;
420
- const fileEnv = loadEnv("development", process.cwd(), "");
450
+ const fileEnv = loadEnv2("development", process.cwd(), "");
421
451
  const id = process.env[idKey] || fileEnv[idKey] || "";
422
452
  const secret = process.env[secretKey] || fileEnv[secretKey] || "";
423
453
  cachedCreds = id && secret ? { id, secret } : null;
@@ -513,16 +543,31 @@ function stripResponseCookies(headers, extraCookies) {
513
543
  headers["set-cookie"] = cookies;
514
544
  }
515
545
  }
516
- function collectBody(req) {
517
- return new Promise((resolve2) => {
546
+ var MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
547
+ function collectBody(req, maxBytes = MAX_REQUEST_BODY_BYTES) {
548
+ return new Promise((resolve2, reject) => {
518
549
  const chunks = [];
519
- req.on("data", (c) => chunks.push(c));
520
- req.on("end", () => resolve2(Buffer.concat(chunks)));
550
+ let total = 0;
551
+ let overflowed = false;
552
+ req.on("data", (c) => {
553
+ if (overflowed) return;
554
+ total += c.length;
555
+ if (total > maxBytes) {
556
+ overflowed = true;
557
+ chunks.length = 0;
558
+ reject(new Error(`Request body exceeds ${maxBytes} bytes`));
559
+ return;
560
+ }
561
+ chunks.push(c);
562
+ });
563
+ req.on("end", () => {
564
+ if (!overflowed) resolve2(Buffer.concat(chunks));
565
+ });
521
566
  });
522
567
  }
523
568
 
524
569
  // src/dev/resolveHosts.ts
525
- import { loadEnv as loadEnv2 } from "vite";
570
+ import { loadEnv as loadEnv3 } from "vite";
526
571
  var HOST_ENV_KEYS = {
527
572
  gateway: "VITE_BSTAGE_GATEWAY_HOST",
528
573
  auth: "VITE_BSTAGE_AUTH_HOST",
@@ -539,7 +584,7 @@ function resolveDevHosts(phase, mode, cwd = process.cwd()) {
539
584
  auth: (tenantId) => getAuthHost(mode, phase, tenantId)
540
585
  };
541
586
  }
542
- const fileEnv = loadEnv2("development", cwd, "");
587
+ const fileEnv = loadEnv3("development", cwd, "");
543
588
  const read = (key) => (process.env[key] || fileEnv[key] || "").trim();
544
589
  const authKey = authKeyFor(mode);
545
590
  const gatewayHost = read(HOST_ENV_KEYS.gateway);
@@ -562,13 +607,13 @@ function resolveDevHosts(phase, mode, cwd = process.cwd()) {
562
607
  }
563
608
 
564
609
  // src/dev/resolvePhase.ts
565
- import { loadEnv as loadEnv3 } from "vite";
610
+ import { loadEnv as loadEnv4 } from "vite";
566
611
  var PHASES = ["dev", "qa", "real", "sandbox"];
567
612
  var DEFAULT_PHASE = "sandbox";
568
613
  function isPhase(value) {
569
614
  return PHASES.includes(value);
570
615
  }
571
- function resolvePhase(explicit, cwd = process.cwd()) {
616
+ function resolvePhase(explicit, cwd = process.cwd(), env = process.env) {
572
617
  if (explicit) {
573
618
  if (!isPhase(explicit)) {
574
619
  throw new Error(
@@ -577,8 +622,8 @@ function resolvePhase(explicit, cwd = process.cwd()) {
577
622
  }
578
623
  return { phase: explicit, source: "flag" };
579
624
  }
580
- const fileEnv = loadEnv3("development", cwd, "");
581
- const fromEnv = process.env.VITE_BSTAGE_PHASE || fileEnv.VITE_BSTAGE_PHASE || "";
625
+ const fileEnv = loadEnv4("development", cwd, "");
626
+ const fromEnv = env.VITE_BSTAGE_PHASE || fileEnv.VITE_BSTAGE_PHASE || "";
582
627
  if (fromEnv) {
583
628
  if (isPhase(fromEnv)) return { phase: fromEnv, source: "env" };
584
629
  return { phase: DEFAULT_PHASE, source: "default", ignoredEnvValue: fromEnv };
@@ -1729,13 +1774,13 @@ async function refreshAccessToken(mode, phase, hosts, tenantId, accessToken, coo
1729
1774
  headers,
1730
1775
  body: Buffer.from(postBody)
1731
1776
  });
1732
- const text = result.body.toString("utf-8");
1733
- const json = JSON.parse(text);
1734
- if (result.statusCode === 200 && json.accessToken) {
1777
+ const json = JSON.parse(result.body.toString("utf-8"));
1778
+ const newToken = json.accessToken ?? json.data?.accessToken;
1779
+ if (result.statusCode === 200 && newToken) {
1735
1780
  console.info(`[bstage] token refreshed (${mode})`);
1736
- return json.accessToken;
1781
+ return newToken;
1737
1782
  }
1738
- console.warn("[bstage] token refresh failed:", result.statusCode, text);
1783
+ console.warn(`[bstage] token refresh failed (mode=${mode}, status=${result.statusCode})`);
1739
1784
  return null;
1740
1785
  } catch {
1741
1786
  return null;
@@ -1821,10 +1866,15 @@ function handleGatewayProxy(mode, phase, hosts, req, res) {
1821
1866
  res.writeHead(result.statusCode, result.headers);
1822
1867
  res.end(result.body);
1823
1868
  } catch (err) {
1824
- const message = err instanceof Error ? err.message : String(err);
1869
+ console.error("[bstage] gateway proxy error:", err instanceof Error ? err.message : err);
1825
1870
  res.writeHead(502, { "Content-Type": "application/json" });
1826
- res.end(JSON.stringify({ message: "Gateway proxy error", error: message }));
1871
+ res.end(JSON.stringify({ message: "Gateway proxy error" }));
1827
1872
  }
1873
+ }).catch((err) => {
1874
+ console.error("[bstage] request body rejected:", err instanceof Error ? err.message : err);
1875
+ if (res.headersSent) return;
1876
+ res.writeHead(413, { "Content-Type": "application/json" });
1877
+ res.end(JSON.stringify({ message: "Request body too large" }));
1828
1878
  });
1829
1879
  }
1830
1880
  function subpathOf(source) {
@@ -1895,18 +1945,22 @@ function handleAuthProxy(mode, phase, hosts, req, res) {
1895
1945
  }
1896
1946
  } else {
1897
1947
  console.warn(
1898
- `[bstage] auth proxy upstream non-2xx (mode=${mode}, status=${result.statusCode}):`,
1899
- textBody.slice(0, 500)
1948
+ `[bstage] auth proxy upstream non-2xx (mode=${mode}, status=${result.statusCode})`
1900
1949
  );
1901
1950
  }
1902
1951
  stripResponseCookies(result.headers, extraCookies);
1903
1952
  res.writeHead(result.statusCode, result.headers);
1904
1953
  res.end(textBody);
1905
1954
  } catch (err) {
1906
- const message = err instanceof Error ? err.message : String(err);
1955
+ console.error("[bstage] auth proxy error:", err instanceof Error ? err.message : err);
1907
1956
  res.writeHead(502, { "Content-Type": "application/json" });
1908
- res.end(JSON.stringify({ message: "Auth proxy error", error: message }));
1957
+ res.end(JSON.stringify({ message: "Auth proxy error" }));
1909
1958
  }
1959
+ }).catch((err) => {
1960
+ console.error("[bstage] request body rejected:", err instanceof Error ? err.message : err);
1961
+ if (res.headersSent) return;
1962
+ res.writeHead(413, { "Content-Type": "application/json" });
1963
+ res.end(JSON.stringify({ message: "Request body too large" }));
1910
1964
  });
1911
1965
  }
1912
1966
  function bstageDevPlugin(options = {}) {
@@ -1920,10 +1974,13 @@ function bstageDevPlugin(options = {}) {
1920
1974
  return {
1921
1975
  name: "bstage-dev",
1922
1976
  enforce: "pre",
1923
- config() {
1977
+ config(config, { mode: mode2 }) {
1924
1978
  const root = process.cwd();
1925
1979
  projectRequire = createRequire(join(root, "package.json"));
1926
1980
  return {
1981
+ // 앱 키 env 별칭 — 소비자 `vite.config.ts`는 이 플러그인만 얹으므로 여기서도 이어 붙인다
1982
+ // (`bstage build`는 프리셋이 맡는다). 상세는 appKeyEnvAlias.ts.
1983
+ define: appKeyEnvDefine(loadProjectEnv(config, mode2)),
1927
1984
  optimizeDeps: {
1928
1985
  exclude: ["@bstage-sdk/core", "@bstage-sdk/react"]
1929
1986
  },
@@ -2035,7 +2092,7 @@ function bstageDevPlugin(options = {}) {
2035
2092
  // src/vite/preset.ts
2036
2093
  function createViteConfig(options = {}) {
2037
2094
  return defineConfig({
2038
- plugins: [react()],
2095
+ plugins: [react(), appKeyEnvAliasPlugin()],
2039
2096
  define: {
2040
2097
  "process.env.NODE_ENV": JSON.stringify("production")
2041
2098
  },
@@ -2062,13 +2119,20 @@ function createViteConfig(options = {}) {
2062
2119
  }
2063
2120
 
2064
2121
  export {
2122
+ APP_KEY_VAR,
2123
+ LEGACY_APP_KEY_VAR,
2065
2124
  LOGIN_PATH,
2125
+ isPublicPhase,
2066
2126
  TRANSLATION_LANGUAGES,
2067
2127
  DEFAULT_TRANSLATION_TIER,
2068
2128
  TRANSLATION_CDN_ORIGIN,
2069
2129
  TRANSLATION_CDN_BASE_KEY,
2070
2130
  TRANSLATION_CDN_BUCKET_PREFIX,
2131
+ PORTAL_HOSTS,
2132
+ PORTAL_HOST_ENV_KEY,
2071
2133
  resolveProjectTarget,
2134
+ appKeyEnvDefine,
2135
+ appKeyEnvAliasPlugin,
2072
2136
  resolvePhase,
2073
2137
  describePhase,
2074
2138
  bstageDevPlugin,