@bstage-sdk/core 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/index.cjs CHANGED
@@ -192,20 +192,32 @@ var resolveBstageFetch = (input, init) => {
192
192
  return hook ? hook(input, init) : globalThis.fetch(input, init);
193
193
  };
194
194
  var APP_ID_PREFIX = "bsa_";
195
- var APP_SECRET_PREFIX = "bsp_";
196
- function warnOnSwappedCredentials(appId, appSecret) {
197
- const appIdLooksLikeSecret = typeof appId === "string" && appId.startsWith(APP_SECRET_PREFIX);
198
- const appSecretLooksLikeId = typeof appSecret === "string" && appSecret.startsWith(APP_ID_PREFIX);
199
- if (!appIdLooksLikeSecret && !appSecretLooksLikeId) return;
195
+ var APP_KEY_PREFIX = "bsp_";
196
+ function warnOnSwappedCredentials(appId, appKey) {
197
+ const appIdLooksLikeKey = typeof appId === "string" && appId.startsWith(APP_KEY_PREFIX);
198
+ const appKeyLooksLikeId = typeof appKey === "string" && appKey.startsWith(APP_ID_PREFIX);
199
+ if (!appIdLooksLikeKey && !appKeyLooksLikeId) return;
200
200
  console.warn(
201
- `[b.stage] appId\uC640 appSecret\uC774 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
202
- appId\uB294 "${APP_ID_PREFIX}", appSecret\uC740 "${APP_SECRET_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.
201
+ `[b.stage] appId\uC640 appKey\uAC00 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
202
+ appId\uB294 "${APP_ID_PREFIX}", appKey\uB294 "${APP_KEY_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.
203
203
  \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC744 \uB2E4\uC2DC \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
204
204
  );
205
205
  }
206
+ function resolveAppKey(config) {
207
+ return config.appKey || config.appSecret || void 0;
208
+ }
209
+ function warnOnMissingAppKey(appKey) {
210
+ if (appKey !== void 0) return;
211
+ console.warn(
212
+ `[b.stage] appKey\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 X-BSTAGE-APP-KEY \uD5E4\uB354 \uC5C6\uC774 \uD638\uCD9C\uB418\uC5B4 API\uAC00 401\uC744 \uB0C5\uB2C8\uB2E4.
213
+ .env\uC758 VITE_BSTAGE_APP_KEY(\uD30C\uD2B8\uB108 \uCF58\uC194\uC758 APP KEY)\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
214
+ );
215
+ }
206
216
  var BstageClient = class {
207
217
  constructor(config) {
208
- warnOnSwappedCredentials(config.appId, config.appSecret);
218
+ const appKey = resolveAppKey(config);
219
+ warnOnMissingAppKey(appKey);
220
+ warnOnSwappedCredentials(config.appId, appKey);
209
221
  const fetchFn = config.fetch ?? resolveBstageFetch;
210
222
  this.http = new HttpClient({
211
223
  baseUrl: config.baseUrl ?? resolveBaseUrl(),
@@ -214,7 +226,8 @@ var BstageClient = class {
214
226
  headers: {
215
227
  "Content-Type": "application/json",
216
228
  "X-BSTAGE-APP-ID": config.appId,
217
- "X-BSTAGE-APP-KEY": config.appSecret,
229
+ // 값이 없으면(UI 전용 템플릿의 빈 .env) 헤더 자체를 빼서 "undefined" 문자열이 실리지 않게 한다.
230
+ ...appKey !== void 0 ? { "X-BSTAGE-APP-KEY": appKey } : {},
218
231
  "X-BSTAGE-TENANT-ID": config.tenantId
219
232
  }
220
233
  });
package/dist/index.d.cts CHANGED
@@ -93,8 +93,21 @@ declare class HttpTimeoutError extends Error {
93
93
  interface BstageConfig {
94
94
  /** 파트너 콘솔에서 발급받은 앱 식별자. */
95
95
  appId: string;
96
- /** 파트너 콘솔에서 발급받은 앱 시크릿. */
97
- appSecret: string;
96
+ /**
97
+ * 파트너 콘솔에서 발급받은 앱 키(APP KEY). 게이트웨이 `X-BSTAGE-APP-KEY` 헤더로 나간다.
98
+ *
99
+ * **브라우저에 노출되는 값이다** — `VITE_` 환경변수로 번들에 평문으로 실리고 요청 헤더에서도
100
+ * 보인다. 비밀값이 아니며, 접근 범위는 게이트웨이가 앱 단위로 제어한다.
101
+ * `appKey`·`appSecret` 중 하나는 있어야 하고, 둘 다 있으면 `appKey`가 우선한다.
102
+ */
103
+ appKey?: string;
104
+ /**
105
+ * `appKey`의 옛 이름. 값은 같다(비밀값이 아니다).
106
+ *
107
+ * @deprecated `appKey`를 쓰세요. 기존 프로젝트 호환용 별칭이며 **0.4.0**(공개 배포판 `@bstage-sdk/core`
108
+ * 기준)에서 제거된다. 다른 배포 라인은 제거 전 MIGRATION 문서에 먼저 고지한다. 둘 다 주면 `appKey`가 우선한다.
109
+ */
110
+ appSecret?: string;
98
111
  /** 템플릿을 적용할 Space(테넌트) 식별자. */
99
112
  tenantId: string;
100
113
  /**
package/dist/index.d.ts CHANGED
@@ -93,8 +93,21 @@ declare class HttpTimeoutError extends Error {
93
93
  interface BstageConfig {
94
94
  /** 파트너 콘솔에서 발급받은 앱 식별자. */
95
95
  appId: string;
96
- /** 파트너 콘솔에서 발급받은 앱 시크릿. */
97
- appSecret: string;
96
+ /**
97
+ * 파트너 콘솔에서 발급받은 앱 키(APP KEY). 게이트웨이 `X-BSTAGE-APP-KEY` 헤더로 나간다.
98
+ *
99
+ * **브라우저에 노출되는 값이다** — `VITE_` 환경변수로 번들에 평문으로 실리고 요청 헤더에서도
100
+ * 보인다. 비밀값이 아니며, 접근 범위는 게이트웨이가 앱 단위로 제어한다.
101
+ * `appKey`·`appSecret` 중 하나는 있어야 하고, 둘 다 있으면 `appKey`가 우선한다.
102
+ */
103
+ appKey?: string;
104
+ /**
105
+ * `appKey`의 옛 이름. 값은 같다(비밀값이 아니다).
106
+ *
107
+ * @deprecated `appKey`를 쓰세요. 기존 프로젝트 호환용 별칭이며 **0.4.0**(공개 배포판 `@bstage-sdk/core`
108
+ * 기준)에서 제거된다. 다른 배포 라인은 제거 전 MIGRATION 문서에 먼저 고지한다. 둘 다 주면 `appKey`가 우선한다.
109
+ */
110
+ appSecret?: string;
98
111
  /** 템플릿을 적용할 Space(테넌트) 식별자. */
99
112
  tenantId: string;
100
113
  /**
package/dist/index.js CHANGED
@@ -138,20 +138,32 @@ var resolveBstageFetch = (input, init) => {
138
138
  return hook ? hook(input, init) : globalThis.fetch(input, init);
139
139
  };
140
140
  var APP_ID_PREFIX = "bsa_";
141
- var APP_SECRET_PREFIX = "bsp_";
142
- function warnOnSwappedCredentials(appId, appSecret) {
143
- const appIdLooksLikeSecret = typeof appId === "string" && appId.startsWith(APP_SECRET_PREFIX);
144
- const appSecretLooksLikeId = typeof appSecret === "string" && appSecret.startsWith(APP_ID_PREFIX);
145
- if (!appIdLooksLikeSecret && !appSecretLooksLikeId) return;
141
+ var APP_KEY_PREFIX = "bsp_";
142
+ function warnOnSwappedCredentials(appId, appKey) {
143
+ const appIdLooksLikeKey = typeof appId === "string" && appId.startsWith(APP_KEY_PREFIX);
144
+ const appKeyLooksLikeId = typeof appKey === "string" && appKey.startsWith(APP_ID_PREFIX);
145
+ if (!appIdLooksLikeKey && !appKeyLooksLikeId) return;
146
146
  console.warn(
147
- `[b.stage] appId\uC640 appSecret\uC774 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
148
- appId\uB294 "${APP_ID_PREFIX}", appSecret\uC740 "${APP_SECRET_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.
147
+ `[b.stage] appId\uC640 appKey\uAC00 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
148
+ appId\uB294 "${APP_ID_PREFIX}", appKey\uB294 "${APP_KEY_PREFIX}"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.
149
149
  \uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC744 \uB2E4\uC2DC \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
150
150
  );
151
151
  }
152
+ function resolveAppKey(config) {
153
+ return config.appKey || config.appSecret || void 0;
154
+ }
155
+ function warnOnMissingAppKey(appKey) {
156
+ if (appKey !== void 0) return;
157
+ console.warn(
158
+ `[b.stage] appKey\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 X-BSTAGE-APP-KEY \uD5E4\uB354 \uC5C6\uC774 \uD638\uCD9C\uB418\uC5B4 API\uAC00 401\uC744 \uB0C5\uB2C8\uB2E4.
159
+ .env\uC758 VITE_BSTAGE_APP_KEY(\uD30C\uD2B8\uB108 \uCF58\uC194\uC758 APP KEY)\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
160
+ );
161
+ }
152
162
  var BstageClient = class {
153
163
  constructor(config) {
154
- warnOnSwappedCredentials(config.appId, config.appSecret);
164
+ const appKey = resolveAppKey(config);
165
+ warnOnMissingAppKey(appKey);
166
+ warnOnSwappedCredentials(config.appId, appKey);
155
167
  const fetchFn = config.fetch ?? resolveBstageFetch;
156
168
  this.http = new HttpClient({
157
169
  baseUrl: config.baseUrl ?? resolveBaseUrl(),
@@ -160,7 +172,8 @@ var BstageClient = class {
160
172
  headers: {
161
173
  "Content-Type": "application/json",
162
174
  "X-BSTAGE-APP-ID": config.appId,
163
- "X-BSTAGE-APP-KEY": config.appSecret,
175
+ // 값이 없으면(UI 전용 템플릿의 빈 .env) 헤더 자체를 빼서 "undefined" 문자열이 실리지 않게 한다.
176
+ ...appKey !== void 0 ? { "X-BSTAGE-APP-KEY": appKey } : {},
164
177
  "X-BSTAGE-TENANT-ID": config.tenantId
165
178
  }
166
179
  });
@@ -101,8 +101,8 @@ Platform → Template 이벤트는 `bubbles: false, composed: false`로 해당
101
101
 
102
102
  ```typescript
103
103
  const client = new BstageClient({
104
- appId: 'app-xxx',
105
- appSecret: 'secret-xxx',
104
+ appId: 'bsa_xxx',
105
+ appKey: 'bsp_xxx',
106
106
  tenantId: 'tenant-xxx',
107
107
  })
108
108
  ```
@@ -112,7 +112,8 @@ const client = new BstageClient({
112
112
  | 옵션 | 타입 | 설명 |
113
113
  | ----------- | --------------------------------------------- | ------------------------------------------------------------ |
114
114
  | `appId` | `string` | 앱 식별자 |
115
- | `appSecret` | `string` | 앱 시크릿 |
115
+ | `appKey` | `string` | 앱 키(APP KEY). `appKey`·`appSecret` 중 하나는 필요합니다. 포털이 발급하고 게이트웨이 `X-BSTAGE-APP-KEY` 헤더로 검증하는 값. **브라우저에 노출되는 값**으로 비밀값이 아닙니다 |
116
+ | `appSecret` | `string` (deprecated) | `appKey`의 옛 이름. 값은 같습니다. 둘 다 주면 `appKey`가 우선하며, 0.4.0(공개 배포판 `@bstage-sdk/core` 기준)에서 제거됩니다 |
116
117
  | `tenantId` | `string` | 테넌트 식별자 |
117
118
  | `phase` | `'dev' \| 'qa' \| 'real' \| 'sandbox'` (선택) | 환경. 현재 미사용 (향후 확장용) |
118
119
  | `baseUrl` | `string` (선택) | base URL 직접 지정. 미지정 시 `resolveBaseUrl()`로 자동 결정 |
@@ -173,7 +174,7 @@ const post = await client.get<PostResponse>('/content/v1/boards/{boardId}/posts/
173
174
 
174
175
  응답 타입(`MenuResponse` 등)은 게이트웨이 API Reference Doc을 보고 필요한 필드만 프로젝트 안에 선언해 쓰는 것을 권장합니다.
175
176
 
176
- 내부적으로 `HttpClient`(Fetch 기반, 인터셉터 지원)를 사용하며, 모든 요청에 `X-BSTAGE-APP-ID`/`X-BSTAGE-APP-KEY`/`X-BSTAGE-TENANT-ID` 헤더를 자동 포함합니다.
177
+ 내부적으로 `HttpClient`(Fetch 기반, 인터셉터 지원)를 사용하며, 모든 요청에 `X-BSTAGE-APP-ID`/`X-BSTAGE-APP-KEY`/`X-BSTAGE-TENANT-ID` 헤더를 자동 포함합니다(앱 키가 없으면 `X-BSTAGE-APP-KEY`는 생략되고 생성 시 콘솔 경고가 납니다).
177
178
 
178
179
  ---
179
180
 
@@ -58,7 +58,7 @@ Vite 빌드라서 `import.meta.env.VITE_BSTAGE_*`는 **빌드 시점에 문자
58
58
  - **대상 판정** — 두 조건을 모두 만족할 때만 점검합니다.
59
59
  1. 번들에 BstageClient의 앱 ID 헤더(`X-BSTAGE-APP-ID`)가 있을 것. API를 쓰지 않는 UI 전용 템플릿은 `client.ts`를 스캐폴드만 하고 import하지 않으면 트리셰이킹으로 사라지므로 대상이 아닙니다.
60
60
  2. 소스가 `import.meta.env.VITE_BSTAGE_*`를 참조할 것. cli 0.40.1 이전 스캐폴드처럼 `client.ts`에 키를 리터럴로 박은 프로젝트는 `.env`가 없는 게 정상이므로(그 전환은 마이그레이션에서 `선택`) 대상이 아닙니다. 참조한 변수만 검사합니다.
61
- - **검사 항목** — 값 누락, 자리표시자(`YOUR_APP_ID` 등) 잔존, appId·appSecret 뒤바뀜, 접두사(`bsa_`/`bsp_`) 불일치
61
+ - **검사 항목** — 값 누락, 자리표시자(`YOUR_APP_ID` 등) 잔존, appId·appKey 뒤바뀜, 접두사(`bsa_`/`bsp_`) 불일치. 앱 키는 `VITE_BSTAGE_APP_KEY`(정본)와 옛 이름 `VITE_BSTAGE_APP_SECRET`을 한 값으로 보며, 한쪽만 있으면 빌드가 다른 이름으로도 읽히게 이어 붙인다
62
62
  - **env 해석** — Vite의 `loadEnv`를 그대로 씁니다. `.env` 파일뿐 아니라 CI가 `process.env`로 주입한 값도 번들에 인라인되므로 파일만 읽으면 오탐이 납니다.
63
63
  - **빌드를 실패시키지 않습니다** — 값이 비어도 번들 자체는 정상적으로 나오고, 여기서 실패시키면 기존 소비자 CI가 깨집니다. 경고는 빌드 로그 맨 끝에 출력됩니다.
64
64
  - 어느 환경에서 발급한 키인지(QA 키를 리얼에 썼는지)는 **감지할 수 없습니다** — 키 형식에 발급 환경이 담기지 않습니다. 그 계열은 [GETTING_STARTED.md](./GETTING_STARTED.md) "9.2 배포 전 점검"의 체크리스트가 담당합니다.
@@ -22,7 +22,7 @@ bstage 커스텀 템플릿의 **색·타이포·그림자**를 bstage 디자인
22
22
  | 유저(유저 플랫폼) | `@bstage-sdk/design/user` | `--user-*` | 라이트/다크 자동 대응 |
23
23
  | 어드민(어드민 플랫폼) | `@bstage-sdk/design/admin` | `--admin-*` | **라이트 전용** |
24
24
 
25
- `bstage init` 스캐폴드에는 `@bstage-sdk/design`가 이미 의존성에 포함된다. 기존 프로젝트라면 `npm i @bstage-sdk/design`로 추가한다. 타깃(user/admin)은 `package.json`의 `bstage.target`으로 정한다(`bstage init --target admin`이 적는다). 선언이 없으면 `user`.
25
+ `bstage init` 스캐폴드에는 `@bstage-sdk/design`이 이미 의존성에 포함된다. 기존 프로젝트라면 `npm i @bstage-sdk/design`로 추가한다. 타깃(user/admin)은 `package.json`의 `bstage.target`으로 정한다(`bstage init --target admin`이 적는다). 선언이 없으면 `user`.
26
26
 
27
27
  ## 사용
28
28
 
@@ -81,7 +81,7 @@ import { color } from '@bstage-sdk/design/user'
81
81
 
82
82
  ## 간격 · 라운드
83
83
 
84
- 간격(spacing)·모서리 반경(radius)은 **전용 토큰이 없다.** 임의 값을 남발하지 말고 **일관된 스케일**을 유지한다 — 간격은 4의 배수(4/8/12/16/24/32…)를 권장하고, radius도 프로젝트 안에서 일관된 값을 재사용한다.
84
+ 간격(spacing)·모서리 반경(radius)은 **전용 토큰이 없다.** 임의 값을 남발하지 말고 **일관된 스케일**을 유지한다 — 간격은 4의 배수(4/8/12/16/24/32…)를 권장하고 radius도 프로젝트 안에서 일관된 값을 재사용한다.
85
85
 
86
86
  ## 테마 (라이트 / 다크)
87
87
 
@@ -277,37 +277,33 @@ VITE_CF_ACCESS_CLIENT_SECRET=
277
277
  - **프로덕션 미포함**: `bstage build`는 이 훅을 거치지 않고, dev 훅도 `ctx.server`(serve)에서만 동작해 빌드 산출물엔 절대 들어가지 않는다.
278
278
  - 유저 대상엔 우하단에 **라이트/다크 토글 버튼**(dev 전용)이 뜬다 — `<html data-bspoke>`를 바꿔 fallback의 다크 모드를 미리본다. 어드민은 light 전용이라 토글이 없다.
279
279
 
280
- ### sandbox phase + Cloudflare WARP (TLS 인증서)
280
+ ### 회사 프록시의 TLS 인스펙션 (`self-signed certificate` 502)
281
281
 
282
- `bstage dev --phase sandbox`에서 게이트웨이 호출이 아래처럼 **502**로 실패한다면:
282
+ `bstage dev`에서 게이트웨이 호출이 아래처럼 **502**로 실패한다면:
283
283
 
284
284
  ```
285
285
  {"message":"Gateway proxy error","error":"Proxy request failed: {space}.sandstage.in/gw/... — self-signed certificate in certificate chain"}
286
286
  ```
287
287
 
288
- **원인은 SDK가 아니라 로컬 Cloudflare WARP(Zero-Trust)입니다.** WARP가 `*.sandstage.in` 트래픽을 TLS 인스펙션(MITM)하면서 인증서를 자체 CA(`Gateway CA - Cloudflare Managed G1`)로 재서명합니다. macOS 키체인은 이 CA를 신뢰하지만(그래서 브라우저·`curl`은 정상), **Node.js는 OS 키체인이 아닌 자체 내장 CA 목록만** 보기 때문에 "모르는 CA = self-signed"로 간주해 거부합니다.
288
+ **원인은 SDK가 아니라 개발 기기의 네트워크 보안 도구일 가능성이 높습니다.** 회사에서 쓰는 Zero Trust 클라이언트나 보안 프록시가 HTTPS 트래픽을 검사하면서 인증서를 자체 CA로 재서명하는 경우가 있습니다. 브라우저와 `curl`은 OS 신뢰 저장소를 보기 때문에 정상이지만, **Node.js는 OS 키체인이 아닌 자체 내장 CA 목록만** 보기 때문에 그 CA를 "모르는 CA = self-signed"로 간주해 거부합니다.
289
289
 
290
- > 사내 전용 phase(`dev`·`qa`)의 게이트웨이는 정식 인증서라 WARP 인스펙션 대상이 아닙니다. 그래서 **이 문제는 `sandbox`(그리고 어드민 `mysandstage.in`)에서만** 나타납니다.
290
+ **해결 — 회사 CA를 Node 신뢰 목록에 추가** (TLS 검증을 끄지 말 것):
291
291
 
292
- **해결 — WARP CA를 Node 신뢰 목록에 추가** (TLS 검증을 끄지 말 것):
292
+ 1. 회사 보안 도구의 루트 CA 인증서를 PEM 파일로 확보합니다. IT·보안 담당에게 문의하거나 OS 신뢰 저장소에서 내보냅니다.
293
+ 2. `NODE_EXTRA_CA_CERTS`로 등록하고 확인합니다.
293
294
 
294
295
  ```bash
295
- # 1) WARP CA를 키체인에서 PEM으로 추출
296
296
  mkdir -p ~/.config/certs
297
- security find-certificate -a -c "Gateway CA" -p /Library/Keychains/System.keychain \
298
- > ~/.config/certs/cloudflare-warp-ca.pem
299
-
300
- # 2) Node가 이 CA를 신뢰하도록 환경변수 등록 (셸 프로파일에 영구 반영)
301
- echo 'export NODE_EXTRA_CA_CERTS="$HOME/.config/certs/cloudflare-warp-ca.pem"' >> ~/.zshrc
297
+ # 확보한 CA를 ~/.config/certs/corporate-ca.pem 으로 저장한 뒤:
298
+ echo 'export NODE_EXTRA_CA_CERTS="$HOME/.config/certs/corporate-ca.pem"' >> ~/.zshrc
302
299
  source ~/.zshrc
303
300
 
304
- # 3) 검증 (cert 에러 없이 상태코드가 찍히면 성공)
301
+ # 검증 (cert 에러 없이 상태코드가 찍히면 성공)
305
302
  node -e "fetch('https://YOUR_SPACE.sandstage.in/gw/').then(r=>console.log('OK',r.status)).catch(e=>console.log('ERR',e.cause?.code||e.message))"
306
303
  ```
307
304
 
308
- - `security ... -c "Gateway CA"`가 매치하지 않으면 `-c "Cloudflare"`로 검색하거나, 키체인 접근에서 인증서 이름을 직접 확인한다.
309
- - **`NODE_TLS_REJECT_UNAUTHORIZED=0`은 쓰지 말 것** — 모든 TLS 검증을 꺼버려 위험하다. 위 방식은 WARP CA만 신뢰 목록에 *추가*하므로 안전하다.
310
- - 이 설정은 **각 개발자 로컬에만** 적용된다(레포·SDK에 반영되지 않음). WARP를 쓰는 팀원은 각자 1회 수행한다.
305
+ - **`NODE_TLS_REJECT_UNAUTHORIZED=0`은 쓰지 말 것** — 모든 TLS 검증을 꺼버려 위험하다. 위 방식은 회사 CA만 신뢰 목록에 *추가*하므로 안전하다.
306
+ - 이 설정은 **각 개발자 로컬에만** 적용된다(레포·SDK에 반영되지 않음).
311
307
 
312
308
  ---
313
309
 
@@ -6,20 +6,20 @@ b.stage 3rd-party 템플릿을 개발하고 배포하는 전체 흐름을 안내
6
6
 
7
7
  - **Node.js** v20 이상
8
8
  - **npm** 또는 **pnpm**
9
- - **BstageClient 인증 정보** (선택) — API 호출에 필요한 `appId`, `appSecret`을 파트너 콘솔에서 발급받아 두면 프로젝트 생성 시 자동으로 설정됩니다.
9
+ - **BstageClient 인증 정보** (선택) — API 호출에 필요한 `appId`, `appKey`(포털의 APP KEY)를 파트너 콘솔에서 발급받아 두면 프로젝트 생성 시 자동으로 설정됩니다.
10
10
 
11
11
  ---
12
12
 
13
13
  ## 개요
14
14
 
15
- b.stage 3rd-party 템플릿은 **Web Component** 기반으로 동작합니다. SDK가 개발자의 컴포넌트를 Custom Element로 변환하여, b.stage 플랫폼 내에서 독립적으로 실행합니다.
15
+ b.stage 3rd-party 템플릿은 **Web Component** 기반으로 동작합니다. SDK가 개발자의 컴포넌트를 Custom Element로 변환하여 b.stage 플랫폼 내에서 독립적으로 실행합니다.
16
16
 
17
17
  **핵심 특징:**
18
18
 
19
19
  - **Shadow DOM 격리**: 템플릿의 스타일이 플랫폼과 충돌하지 않습니다
20
20
  - **IIFE 번들**: 별도의 모듈 로더 없이 `<script>` 태그로 로드됩니다
21
21
  - **이벤트 기반 통신**: 플랫폼과 CustomEvent를 통해 통신합니다
22
- - **프레임워크 자유**: React, Vanilla JS를 지원하며, Vue/Svelte도 지원 예정입니다
22
+ - **프레임워크 자유**: React, Vanilla JS를 지원하며 Vue/Svelte도 지원 예정입니다
23
23
 
24
24
  설계 원리와 내부 동작은 [SDK_ARCHITECTURE.md](./SDK_ARCHITECTURE.md)를 참고하세요.
25
25
 
@@ -42,7 +42,7 @@ npx @bstage-sdk/cli@latest init
42
42
 
43
43
  API 키는 나중에 설정해도 됩니다. "API 키가 있으신가요?" 질문에 `No`를 선택하면 자리표시자(`YOUR_APP_ID` 등)로 `src/shared/client.ts`가 생성됩니다.
44
44
 
45
- 완료되면 SDK 최신 버전 조회 → 파일 생성 → git 초기화 → 의존성 설치가 자동으로 실행됩니다. `.npmrc`, `vite.config.ts`, `tsconfig.json`, `eslint.config.js`, 에이전트용 `AGENTS.md` / `CLAUDE.md` 등 모든 보일러플레이트가 자동 생성됩니다. CI 워크플로우는 관리도구가 별도 구성합니다 — init은 `.github/workflows/` 파일을 만들지 않습니다. 상세 내용은 [INIT.md](./INIT.md)를 참고하세요.
45
+ 완료되면 SDK 최신 버전 조회 → 파일 생성 → git 초기화 → 의존성 설치가 자동으로 실행됩니다. `.npmrc`, `vite.config.ts`, `tsconfig.json`, `eslint.config.js`, 에이전트용 `AGENTS.md` / `CLAUDE.md` 등 모든 보일러플레이트가 자동 생성됩니다. CI 워크플로우는 관리도구가 별도 구성합니다. init은 `.github/workflows/` 파일을 만들지 않습니다. 상세 내용은 [INIT.md](./INIT.md)를 참고하세요.
46
46
 
47
47
  > 프롬프트 없이 실행하려면 `--yes` 플래그를 사용하세요.
48
48
  >
@@ -108,7 +108,7 @@ npm run dev
108
108
 
109
109
  - 파일명은 반드시 `template.tsx`여야 합니다. 빌드 CLI와 로컬 라우팅이 이 패턴으로 템플릿을 자동 탐색합니다.
110
110
  - **페이지**는 `src/pages/{경로}/template.tsx` — 폴더 구조가 곧 배포 경로가 됩니다.
111
- - **위젯**은 `src/slots/{아무 이름}/template.tsx` — 어느 자리에 붙을지는 `createTemplate`의 `slot` 옵션이 정합니다.
111
+ - **위젯**은 `src/slots/{아무 이름}/template.tsx`입니다. 어느 자리에 붙을지는 `createTemplate`의 `slot` 옵션이 정합니다.
112
112
 
113
113
  ---
114
114
 
@@ -116,7 +116,7 @@ npm run dev
116
116
 
117
117
  ### createTemplate
118
118
 
119
- 각 템플릿은 React 컴포넌트를 작성한 후, `createTemplate()`을 호출하여 Web Component로 등록합니다.
119
+ 각 템플릿은 React 컴포넌트를 작성한 후 `createTemplate()`을 호출하여 Web Component로 등록합니다.
120
120
 
121
121
  ```tsx
122
122
  // src/pages/hello/template.tsx
@@ -146,7 +146,7 @@ createTemplate(BmfHelloTemplate, {
146
146
  | `type` | `string` | | 관리도구에서 사용하는 템플릿 타입 식별자 |
147
147
  | `shadow` | `boolean` | | Shadow DOM 사용 여부 (기본값: `true`) |
148
148
 
149
- > 배치는 소스 위치가 정합니다 — 페이지는 `src/pages/` 폴더 구조, 위젯은 `slot` 옵션. 풀페이지 레이아웃(topBar/bottomBar)은 관리도구가 소유하므로 템플릿 코드에서 선언하지 않습니다. 자세한 내용은 [BUILD_SYSTEM.md](./BUILD_SYSTEM.md)·[SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)를 참고하세요.
149
+ > 배치는 소스 위치가 정합니다(페이지는 `src/pages/` 폴더 구조, 위젯은 `slot` 옵션). 풀페이지 레이아웃(topBar/bottomBar)은 관리도구가 소유하므로 템플릿 코드에서 선언하지 않습니다. 자세한 내용은 [BUILD_SYSTEM.md](./BUILD_SYSTEM.md)·[SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)를 참고하세요.
150
150
 
151
151
  > - 컴포넌트는 반드시 `export default`로 내보내야 합니다
152
152
  > - `createTemplate()`은 반드시 모듈 최상위 레벨에서 호출해야 합니다. 함수 내부에서 호출하면 빌드 시 메타데이터를 추출할 수 없습니다.
@@ -197,7 +197,7 @@ createTemplate(CurationBanner, {
197
197
  })
198
198
  ```
199
199
 
200
- `slot`에 넣을 수 있는 값은 편집기 자동완성으로 볼 수 있고, 전체 목록은 [SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)에 있습니다. 오타가 있으면 `bstage build`가 후보와 함께 막아줍니다.
200
+ `slot`에 넣을 수 있는 값은 편집기 자동완성으로 볼 수 있고 전체 목록은 [SLOT_CATALOG_V2.md](./SLOT_CATALOG_V2.md)에 있습니다. 오타가 있으면 `bstage build`가 후보와 함께 막아줍니다.
201
201
 
202
202
  > 동적 경로(`src/pages/artist/[id]/`)는 아직 지원하지 않습니다.
203
203
 
@@ -236,7 +236,7 @@ function HomeTemplate() {
236
236
 
237
237
  > ⚠️ **`import './home.css'`(문자열이 아닌 부수효과 import)는 배포 산출물에서 사라집니다.**
238
238
  >
239
- > 빌드가 CSS를 별도 `.css` 에셋으로 내는데 산출물에는 `template.js`만 실리기 때문입니다. **로컬 `npm run dev` 화면에서는 적용되므로** 눈치채기 어렵고, 배포한 뒤에야 "스타일이 통째로 빠졌다"로 드러납니다. CSS 파일로 관리하고 싶다면 반드시 `?inline`을 붙여 위 두 방법 중 하나로 넘기세요.
239
+ > 빌드가 CSS를 별도 `.css` 에셋으로 내는데 산출물에는 `template.js`만 실리기 때문입니다. **로컬 `npm run dev` 화면에서는 적용되므로** 눈치채기 어렵고 배포한 뒤에야 "스타일이 통째로 빠졌다"로 드러납니다. CSS 파일로 관리하고 싶다면 반드시 `?inline`을 붙여 위 두 방법 중 하나로 넘기세요.
240
240
  >
241
241
  > 반대로 **`styles` 옵션은 로컬 개발 화면에서 안 보입니다** — 개발 화면은 템플릿을 Shadow DOM 없이 그냥 렌더하기 때문입니다. 최종 모습은 `npx bstage build` 산출물로 확인하세요.
242
242
 
@@ -259,12 +259,14 @@ import { BstageClient } from '@bstage-sdk/core'
259
259
 
260
260
  export const client = new BstageClient({
261
261
  appId: 'bsa_xxxxx',
262
- appSecret: 'bsp_xxxxx',
262
+ appKey: 'bsp_xxxxx',
263
263
  tenantId: 'myspace',
264
264
  })
265
265
  ```
266
266
 
267
- - 게이트웨이 base URL은 클라이언트가 임베드된 **페이지의 origin**(`location.origin + /gw`)으로 자동 결정됩니다. `{tenant}.sandstage.in`에 배포되면 그 환경의 게이트웨이로 가므로, 환경별로 다시 빌드·설정할 필요가 없습니다.
267
+ - `appKey`는 포털 화면의 **APP KEY**, 게이트웨이 헤더 `X-BSTAGE-APP-KEY`와 같은 값입니다. `VITE_` 환경변수로 번들에 평문으로 실려 브라우저에서 보이는 값이라 **비밀값이 아닙니다** — 접근 범위는 게이트웨이가 앱 단위로 제어합니다. 옛 이름 `appSecret`(`.env`의 `VITE_BSTAGE_APP_SECRET`)은 별칭으로 당분간 동작합니다([마이그레이션 가이드](./MIGRATION.md) 참고).
268
+
269
+ - 게이트웨이 base URL은 클라이언트가 임베드된 **페이지의 origin**(`location.origin + /gw`)으로 자동 결정됩니다. `{tenant}.sandstage.in`에 배포되면 그 환경의 게이트웨이로 가므로 환경별로 다시 빌드·설정할 필요가 없습니다.
268
270
  - 로컬 개발에서는 **dev 서버**가 `.env`의 `VITE_BSTAGE_PHASE`(→ `vite.config.ts`의 `bstageDevPlugin`)를 보고 해당 phase 게이트웨이로 프록시합니다. 이 phase는 **dev 서버 설정**이지 `BstageClient`의 옵션이 아닙니다.
269
271
  - 특정 게이트웨이를 직접 지정하려면 `baseUrl` 옵션을 사용합니다(예: 플랫폼 임베드가 아닌 standalone 앱).
270
272
 
@@ -279,7 +281,7 @@ export const client = new BstageClient({
279
281
 
280
282
  ### 6.3 요청 예시
281
283
 
282
- **호출할 경로와 응답 모양은 게이트웨이 API Reference Doc을 보고 정합니다.** SDK는 경로 목록을 들고 있지 않으므로 자동완성이 뜨지 않고, 응답 타입도 제네릭으로 직접 지정합니다. 생략하면 `unknown`이라 필드를 읽을 때 타입 오류가 납니다.
284
+ **호출할 경로와 응답 모양은 게이트웨이 API Reference Doc을 보고 정합니다.** SDK는 경로 목록을 들고 있지 않으므로 자동완성이 뜨지 않고 응답 타입도 제네릭으로 직접 지정합니다. 생략하면 `unknown`이라 필드를 읽을 때 타입 오류가 납니다.
283
285
 
284
286
  ```ts
285
287
  import { client } from '../shared/client'
@@ -364,11 +366,11 @@ try {
364
366
  }
365
367
  ```
366
368
 
367
- `HttpError.message`는 `HTTP 404`처럼 상태 코드만 담습니다. 원인을 분기할 정보는 `e.status`·`e.data`에 있습니다. **`e.data`를 화면에 그대로 띄우지 마세요** — 서버 응답 원문이라 내부 정보가 섞일 수 있습니다. 사용자에게 보여줄 문구는 `e.status`로 갈라 직접 정하세요.
369
+ `HttpError.message`는 `HTTP 404`처럼 상태 코드만 담습니다. 원인을 분기할 정보는 `e.status`·`e.data`에 있습니다. **`e.data`를 화면에 그대로 띄우지 마세요.** 서버 응답 원문이라 내부 정보가 섞일 수 있습니다. 사용자에게 보여줄 문구는 `e.status`로 갈라 직접 정하세요.
368
370
 
369
371
  ### 6.6 파일 업로드 — presigned URL
370
372
 
371
- 이미지·동영상 업로드는 **4단계**입니다. 중간 단계를 건너뛰면 업로드는 성공한 것처럼 보이는데 나중에 404가 나므로, 순서를 지켜야 합니다.
373
+ 이미지·동영상 업로드는 **4단계**입니다. 중간 단계를 건너뛰면 업로드는 성공한 것처럼 보이는데 나중에 404가 나므로 순서를 지켜야 합니다.
372
374
 
373
375
  | 단계 | 호출 | 받는 것 |
374
376
  | ------------ | -------------------------------------------------- | ------------------------------------------------- |
@@ -423,7 +425,7 @@ API 호출은 base URL을 SDK가 알아서 해석하므로 환경을 신경 쓸
423
425
  | real | `{space}.bstage.in` | `{space}.mybstage.in` |
424
426
  | sandbox | `{space}.sandstage.in` | `{space}.mysandstage.in` |
425
427
 
426
- 규칙은 하나입니다 — **어드민은 `my` 접두사**가 붙습니다.
428
+ 규칙은 하나입니다. **어드민은 `my` 접두사**가 붙습니다.
427
429
 
428
430
  > 사내 전용 phase(`dev`·`qa`)는 도메인이 다르고 이 문서에 적지 않습니다. 사내에서 개발한다면 사내 문서를 참고하세요. 아래 코드는 phase와 무관하게 동작합니다.
429
431
 
@@ -441,9 +443,9 @@ function toAdminHost(hostname = location.hostname): string {
441
443
  }
442
444
  ```
443
445
 
444
- phase 자체가 필요하면 호스트 조각으로 판정합니다 — 세 번째 조각이 `dev`·`qa`면 그 phase, `sandstage.in`으로 끝나면 sandbox, 그 밖은 real입니다.
446
+ phase 자체가 필요하면 호스트 조각으로 판정합니다. 세 번째 조각이 `dev`·`qa`면 그 phase, `sandstage.in`으로 끝나면 sandbox, 그 밖은 real입니다.
445
447
 
446
- > `BstageConfig.phase`는 예약 필드이고 **런타임에 쓰이지 않습니다.** 게이트웨이 주소는 이 값이 아니라 현재 origin으로 결정되므로, 링크를 만들 때 이 필드를 읽지 마세요.
448
+ > `BstageConfig.phase`는 예약 필드이고 **런타임에 쓰이지 않습니다.** 게이트웨이 주소는 이 값이 아니라 현재 origin으로 결정되므로 링크를 만들 때 이 필드를 읽지 마세요.
447
449
 
448
450
  ---
449
451
 
@@ -469,7 +471,7 @@ function MySlotWidget() {
469
471
 
470
472
  ### 7.2 useNavigation
471
473
 
472
- `useNavigation` 훅을 사용하여 플랫폼에 네비게이션 이벤트를 전달할 수 있습니다. 플랫폼 임베드 환경에서는 PlatformBridge로 전달되고, 로컬 dev 환경에서는 브라우저 네이티브 API(`location.href`, `history.back`, `window.open`)로 fallback됩니다.
474
+ `useNavigation` 훅을 사용하여 플랫폼에 네비게이션 이벤트를 전달합니다. 플랫폼 임베드 환경에서는 PlatformBridge로 전달되고 로컬 dev 환경에서는 브라우저 네이티브 API(`location.href`, `history.back`, `window.open`)로 fallback됩니다.
473
475
 
474
476
  ```tsx
475
477
  import { useNavigation } from '@bstage-sdk/react'
@@ -519,7 +521,7 @@ function MyComponent() {
519
521
 
520
522
  b.stage 어드민(`{space}.mybstage.in`)에 올라가는 템플릿도 **같은 패키지·같은 명령·같은 스캐폴드**로 만듭니다. 별도 SDK가 없습니다.
521
523
 
522
- 프로젝트를 만들 때 `--target admin`을 주면 `package.json`에 아래가 기록되고, 그 뒤로는 CLI가 이 값을 보고 어드민 디자인 토큰·가이드를 안내합니다.
524
+ 프로젝트를 만들 때 `--target admin`을 주면 `package.json`에 아래가 기록되고 그 뒤로는 CLI가 이 값을 보고 어드민 디자인 토큰·가이드를 안내합니다.
523
525
 
524
526
  ```json
525
527
  {
@@ -533,11 +535,11 @@ npx @bstage-sdk/cli@latest init --target admin
533
535
 
534
536
  ### 한 레포는 유저 아니면 어드민 하나입니다
535
537
 
536
- `target`이 **프로젝트 단위**인 이유는 페이지의 배포 경로가 `src/pages/` 폴더 구조이기 때문입니다. 유저 `/settings`와 어드민 `/settings`를 한 레포에 두려면 둘 다 `src/pages/settings/template.tsx`여야 하는데, 파일이 하나뿐이라 부딪힙니다. 그래서 유저 템플릿과 어드민 템플릿은 **레포를 나눠서** 개발합니다.
538
+ `target`이 **프로젝트 단위**인 이유는 페이지의 배포 경로가 `src/pages/` 폴더 구조이기 때문입니다. 유저 `/settings`와 어드민 `/settings`를 한 레포에 두려면 둘 다 `src/pages/settings/template.tsx`여야 하는데 파일이 하나뿐이라 부딪힙니다. 그래서 유저 템플릿과 어드민 템플릿은 **레포를 나눠서** 개발합니다.
537
539
 
538
540
  ### `target`이 정하는 것
539
541
 
540
- 런타임 동작은 갈리지 않습니다 — 등록 경로도 어댑터도 하나입니다. `target`은 도구가 읽는 값입니다.
542
+ 등록 경로도 어댑터도 하나이므로 런타임 동작은 갈리지 않습니다. `target`은 도구가 읽는 값입니다.
541
543
 
542
544
  - 디자인 토큰: 어드민 프로젝트는 `@bstage-sdk/design/admin`(`--admin-*`), 유저는 `/user`(`--user-*`)를 씁니다. `bstage dev`가 해당 토큰의 fallback을 깝니다.
543
545
  - 에이전트 가이드(AGENTS.md 관리 영역·`bstage-template` 스킬)가 어느 서브패스를 안내할지.
@@ -552,19 +554,67 @@ npx @bstage-sdk/cli@latest init --target admin
552
554
 
553
555
  ### 아직 안 되는 것
554
556
 
555
- **어드민 API 호출 경로는 없습니다.** `BstageClient`는 게이트웨이(유저단 API)만 지원하며, 어드민용 게이트웨이는 아직 열리지 않았습니다. 어드민 데이터가 필요한 템플릿은 그때까지 기다려야 합니다 — 경로를 임의로 만들어 호출하지 마세요.
557
+ **어드민 API 호출 경로는 없습니다.** `BstageClient`는 게이트웨이(유저단 API)만 지원하며 어드민용 게이트웨이는 아직 열리지 않았습니다. 어드민 데이터가 필요한 템플릿은 그때까지 기다려야 합니다 — 경로를 임의로 만들어 호출하지 마세요.
556
558
 
557
559
  ---
558
560
 
559
561
  ## 9. 배포
560
562
 
561
- 배포는 관리도구가 이 레포의 GitHub Actions 워크플로우를 `workflow_dispatch`로 트리거하여 수행합니다. SDK는 워크플로우 파일을 생성·관리하지 않습니다 — 개발자는 코드 작성·push에만 집중하면 됩니다.
563
+ 배포는 포털이 이 레포의 기본 브랜치 HEAD를 clone해 빌드하고, 그 산출물을 배치(페이지·슬롯)에 적용하는 것으로 이뤄집니다. SDK는 워크플로우 파일을 생성·관리하지 않습니다. 포털 화면에서 하거나, 아래 `bstage deploy`로 터미널에서 합니다.
564
+
565
+ ### 9.0 터미널에서 배포하기 — `bstage deploy`
566
+
567
+ 포털 화면(`새 버전 빌드` → `버전 변경` → `게시하기`)과 같은 일을 CLI로 합니다. **포털은 GitHub 기본 브랜치의 HEAD를 clone해 빌드**하므로 로컬 산출물은 올라가지 않습니다 — 커밋·push가 먼저입니다.
568
+
569
+ 1. `npx bstage login` — 터미널에 코드(`XXXX-XXXX`)가 뜨고 브라우저가 열립니다. 포털에 로그인한 뒤 **코드가 같은지 확인**하고 등급(`deploy`)과 스코프(스테이지 선택, 또는 조직 Admin이면 **이 조직의 모든 스테이지**)를 골라 승인하면 터미널이 자동으로 로그인됩니다(토큰은 `~/.config/bstage/credentials.json`에 0600 권한으로 저장되며, 포털이 정한 기간이 지나면 만료됩니다 — 만료일은 로그인 완료 시 터미널에 표시됩니다).
570
+ 2. CI·브라우저 없는 환경은 포털 **설정 › CLI 토큰**에서 발급한 토큰을 `BSTAGE_TOKEN`으로 넘깁니다. 포털 주소는 공개 phase(`real`·`sandbox`)는 CLI에 내장돼 있고, 사내 phase(`dev`·`qa`)는 `.env`의 `VITE_BSTAGE_PORTAL_HOST`로 지정합니다 — 어느 phase를 쓸지는 `.env`의 `VITE_BSTAGE_PHASE`(없으면 `sandbox`)로 정해지며 `bstage dev`와 같은 규칙입니다.
571
+ 3. 프로젝트 디렉터리에서 `npx bstage link` — 조직·스테이지·연결 레포 선택 (`.bstage/project.json`, gitignore 대상)
572
+ 4. `git push` 후 `npx bstage deploy` — 더티 트리·브랜치·push 여부 점검 → 빌드 → 로그 → 배치 적용 확인. **`deploy`는 게시까지 하지 않습니다** — 게시 여부를 바꾸려면 `bstage publish on`/`off`를 따로 실행하세요.
573
+
574
+ **조직마다 한 번 로그인합니다.** CLI 토큰은 조직 하나에 묶여 있어, 여러 조직의 스테이지를 오가려면 조직마다 `bstage login` 을 한 번씩 해야 합니다. 자격증명 파일(`~/.config/bstage/credentials.json`)은 **포털 주소 아래 조직별로** 토큰을 담으므로 두 번째 로그인이 첫 토큰을 덮어쓰지 않습니다. 명령이 쓸 토큰은 `.bstage/project.json` 의 조직으로 골라집니다(CI는 `BSTAGE_ORG`, `link` 는 `--org`). **조직이 정해져 있으면 그 조직 토큰만 씁니다** — 다른 조직 토큰으로 대신하지 않고, 저장된 조직 목록과 "이 조직으로 `bstage login` 하세요" 안내와 함께 종료 코드 3으로 끝납니다. 예외는 하나, 예전 형식(조직을 담지 않던 파일)에서 옮겨 온 토큰이 그 포털의 유일한 토큰일 때입니다 — 이때는 그대로 쓰이고, 다음에 그 포털로 로그인하면 조직이 붙은 토큰으로 대체됩니다. 무엇이 저장돼 있는지는 `bstage whoami` 가 조직·등급·스코프(`이 조직의 모든 스테이지` 또는 스테이지 개수)·만료로 보여 주고, `bstage logout` 은 기본적으로 그 포털의 모든 조직 토큰을 지웁니다(`--org <id>` 로 하나만). 붙여 넣기 경로(`--token`·`BSTAGE_TOKEN`)는 토큰만으로는 어느 조직 것인지 알 수 없어 `--org <id>` 또는 `BSTAGE_ORG` 를 함께 줘야 합니다.
575
+
576
+ **포털 주소는 허용된 곳에서만 옵니다.** `.bstage/project.json`과 `.env`는 저장소 안 파일이라 PR 하나로 바뀝니다 — 토큰을 `Authorization` 헤더로 보내는 주소를 그런 파일이 마음대로 정하면 적대적 PR이 CI 토큰을 다른 서버로 끌어갈 수 있습니다. 그래서 링크 파일의 `portalUrl`은 **허용 목록**(CLI에 내장된 공개 포털 주소 + `--portal`로 준 주소 + `BSTAGE_PORTAL_URL` + `BSTAGE_TOKEN`이 없을 때의 `.env` `VITE_BSTAGE_PORTAL_HOST`) 안의 오리진일 때만 쓰이고, 밖이면 요청을 보내지 않고 종료 코드 2로 끝납니다. **`BSTAGE_TOKEN`이 설정된 환경(CI)에서는 한 단계 더 좁아집니다** — 포털 주소는 `--portal`·`BSTAGE_PORTAL_URL`·공개 phase 매핑으로만 정해지고, 링크 파일과 `.env`의 주소는 아예 쓰이지 않습니다. CI에서는 `BSTAGE_PORTAL_URL`을 명시하세요. `bstage login`도 링크 파일을 보지 않습니다.
577
+
578
+ CI에서는 `BSTAGE_TOKEN`·`BSTAGE_PORTAL_URL`(또는 `VITE_BSTAGE_PHASE`)·`BSTAGE_ORG`·`BSTAGE_SPACE`·`BSTAGE_REPO`·`BSTAGE_SURFACE`를 환경변수로 주고 `bstage deploy --yes --json`을 실행합니다 — `--yes`는 확인 프롬프트를 생략하고, `--json`은 사람이 읽는 안내 없이 구조화된 결과만 stdout에 남겨 스크립트에서 파싱하기 좋습니다.
579
+
580
+ | 종료 코드 | 의미 |
581
+ | --------- | ------------------------------------------ |
582
+ | 0 | 성공 |
583
+ | 1 | 실패 |
584
+ | 2 | 사전조건 미충족 (링크 안 됨, 더티 트리 등) |
585
+ | 3 | 인증 실패 (토큰 없음·만료) |
586
+ | 4 | 충돌 (다른 배포와 경합) |
587
+ | 5 | 요청 제한 (재시도 필요) |
588
+
589
+ | 명령 | 하는 일 |
590
+ | ------------------------------------------ | ------------------------------- |
591
+ | `login` / `logout` / `whoami` | 토큰 등록·삭제·확인 |
592
+ | `link` | 디렉터리 ↔ 조직/스테이지/레포 |
593
+ | `list` | 배치 상태·라이브 커밋·최근 빌드 |
594
+ | `logs [buildId] [-f]` | 빌드 로그 |
595
+ | `deploy [--placement] [--yes] [--no-wait]` | 빌드 → 라이브 적용 |
596
+ | `rollback [buildId]` | 이전 성공 빌드 재적용 |
597
+ | `publish on\|off` | 게시 토글 (라이브 버전은 유지) |
598
+
599
+ 확인 프롬프트가 있는 명령(`deploy`·`rollback`·`publish`·`link`)에서 `--json`은 프롬프트와 섞일 수 없어 **`--yes`와 함께**만 쓸 수 있습니다(`list`·`logs`·`whoami`는 `--json`만으로 됩니다). 출력은 stdout에 객체 **하나**뿐입니다. `deploy`·`rollback`·`publish`는 같은 모양을 씁니다 — 명령마다 다르게 파싱하지 않아도 됩니다.
600
+
601
+ | 키 | 내용 |
602
+ | ------------- | ----------------------------------------------------- |
603
+ | `build` | 이번 빌드 (`deploy`에만 있음) |
604
+ | `changed[]` | 실제로 바뀐 배치 |
605
+ | `unchanged[]` | 이미 그 상태라 건드리지 않은 배치 |
606
+ | `conflicts[]` | 다른 변경이 먼저 적용돼 409로 남은 배치 (종료 코드 4) |
607
+ | `failed[]` | 그 외 실패 (종료 코드 1) |
608
+
609
+ 네 배열은 비어 있어도 항상 나옵니다. 각 항목은 `{ id, label, from?, to?, message? }`이고 `from`·`to`는 라이브 빌드 id입니다(`publish`는 게시 여부만 바꾸므로 없습니다). `bstage logs --json`은 `{ build, log }`이며, `-f`와 함께 써도 진행 중 로그는 stderr로 흐르고 stdout에는 이 객체 하나만 나갑니다.
610
+
611
+ 배치 자체를 만들거나 지우는 일은 포털 화면에서만 합니다.
562
612
 
563
613
  로컬에서 산출물을 확인하고 싶으면 `npx bstage build`를 실행합니다. 페이지는 `dist/{경로}/template.js`, 위젯은 `dist/{슬롯 id}/template.js`로 나옵니다.
564
614
 
565
615
  ### 9.1 인증 값은 빌드 시점에 번들로 들어갑니다
566
616
 
567
- `.env`의 `VITE_BSTAGE_*` 값은 **빌드할 때 번들 안에 문자열로 박힙니다.** 배포한 뒤에 `.env`를 고쳐도 반영되지 않고, 값을 바꿨으면 반드시 다시 빌드해야 합니다.
617
+ `.env`의 `VITE_BSTAGE_*` 값은 **빌드할 때 번들 안에 문자열로 박힙니다.** 배포한 뒤에 `.env`를 고쳐도 반영되지 않습니다. 값을 바꿨으면 반드시 다시 빌드해야 합니다.
568
618
 
569
619
  `.env` 없이 빌드하면 번들에 `undefined`가 박혀 배포 후 모든 API 호출이 401로 실패합니다. `bstage build`는 BstageClient를 쓰면서 인증 값을 `.env`로 주입하는 템플릿에 한해 빌드 끝에 이걸 점검해 알려줍니다.
570
620
 
@@ -577,22 +627,22 @@ npx @bstage-sdk/cli@latest init --target admin
577
627
 
578
628
  ### 9.2 배포 전 점검 — 증상으로 원인 되짚기
579
629
 
580
- 아래 항목은 **SDK가 감지할 수 없습니다.** 키 형식(`bsa_`/`bsp_`)은 ID·시크릿 구분일 뿐 어느 환경에서 발급했는지를 담지 않고, 나머지는 BE·콘솔 설정이라 코드에 흔적이 없습니다. 리얼 배포 후 아래 에러를 만나면 이 표로 되짚으세요.
630
+ 아래 항목은 **SDK가 감지할 수 없습니다.** 키 형식(`bsa_`/`bsp_`)은 ID·시크릿 구분일 뿐 어느 환경에서 발급했는지를 담지 않습니다. 나머지는 BE·콘솔 설정이라 코드에 흔적이 없습니다. 리얼 배포 후 아래 에러를 만나면 이 표로 되짚으세요.
581
631
 
582
632
  | 증상 | 원인 | 조치 | 소관 |
583
633
  | ----------------------- | ---------------------------------------- | ----------------------------------------------- | ------------ |
584
- | `401 Unauthorized` | 다른 환경(QA 등)의 키로 배포 / 키 미발급 | 배포할 환경에서 발급한 appId·appSecret인지 확인 | 파트너 콘솔 |
634
+ | `401 Unauthorized` | 다른 환경(QA 등)의 키로 배포 / 키 미발급 | 배포할 환경에서 발급한 appId·appKey인지 확인 | 파트너 콘솔 |
585
635
  | `403 Forbidden` | Space ID가 프로젝트에 미등록 | BE에 Space ID 등록 요청 | BE |
586
636
  | `400 MISSING_ORIGIN` | 키의 Allowed Origins에 배포 도메인 없음 | 배포 도메인을 키 설정에 추가 요청 | BE (키 설정) |
587
637
  | 템플릿이 화면에 안 뜸 | 콘솔 "커스텀 템플릿 사용" 토글이 꺼짐 | 콘솔에서 토글 확인 | 콘솔 |
588
638
  | 배포 자체가 안 됨 | 데브포털 접근·리얼 배포 권한 없음 | 권한 요청 | DevOps |
589
639
  | 임베드(iframe)가 차단됨 | CSP `frame-ancestors`에 도메인 없음 | 도메인 추가 요청 | BE·보안 |
590
640
 
591
- **QA에서 됐다고 리얼에서 되지 않습니다.** 위 항목은 전부 환경마다 따로 설정되고, 하나씩 막힐 때마다 요청·대기가 반복되면 며칠이 걸립니다. 처음 리얼로 나갈 때는 위 표의 BE·콘솔 항목을 **한 번에 모아서** 요청하세요.
641
+ **QA에서 됐다고 리얼에서 되지 않습니다.** 위 항목은 전부 환경마다 따로 설정됩니다. 하나씩 막힐 때마다 요청·대기가 반복되면 며칠이 걸립니다. 처음 리얼로 나갈 때는 위 표의 BE·콘솔 항목을 **한 번에 모아서** 요청하세요.
592
642
 
593
643
  ### 9.3 보안성 검토 — 어떤 통제가 어디에 있나
594
644
 
595
- 보안성 검토에서 "API 남용을 막고 있나", "에러에 내부 정보가 노출되지 않나" 같은 질문을 받습니다. 통제 지점이 SDK 안이 아닌 것이 많아, 어디를 가리켜 답해야 하는지 정리했습니다.
645
+ 보안성 검토에서 "API 남용을 막고 있나", "에러에 내부 정보가 노출되지 않나" 같은 질문을 받습니다. 통제 지점이 SDK 안이 아닌 것이 많아 어디를 가리켜 답해야 하는지 정리했습니다.
596
646
 
597
647
  | 요구 | 통제 지점 | 답변 근거 |
598
648
  | --------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
package/docs/INIT.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 1. 개요
4
4
 
5
- `bstage init`은 서드파티 개발자가 b.stage 템플릿 프로젝트를 즉시 시작할 수 있도록 보일러플레이트를 생성하는 CLI 명령어입니다.
5
+ `bstage init`은 보일러플레이트를 생성하는 CLI 명령어입니다. 서드파티 개발자는 이 명령어로 b.stage 템플릿 프로젝트를 즉시 시작할 수 있습니다.
6
6
 
7
7
  ```bash
8
8
  npx @bstage-sdk/cli@latest init
@@ -31,7 +31,7 @@ Node.js v20 이상과 npm(또는 pnpm)만 있으면 됩니다. SDK 패키지는
31
31
  | 4 | 패키지 매니저 | O | `npm` 또는 `pnpm` |
32
32
  | 5 | API 키 설정 여부 | - | "없어도 시작할 수 있어요" |
33
33
  | 6 | APP-ID | - | API 키가 있는 경우만 |
34
- | 7 | Secret | - | API 키가 있는 경우만 |
34
+ | 7 | APP KEY | - | API 키가 있는 경우만 |
35
35
 
36
36
  ### 레포명 결정 로직
37
37
 
@@ -102,7 +102,7 @@ npx @bstage-sdk/cli@latest init --yes --space bmf --phase dev
102
102
  npx @bstage-sdk/cli@latest init --yes --space my-space --phase dev --target admin
103
103
  ```
104
104
 
105
- 어드민 템플릿 작성법은 [GETTING_STARTED.md](./GETTING_STARTED.md)의 "어드민 템플릿" 절을 참고한다.
105
+ 어드민 템플릿 작성법은 [GETTING_STARTED.md](./GETTING_STARTED.md)의 "어드민 템플릿" 절을 참고하세요.
106
106
 
107
107
  ---
108
108
 
@@ -156,7 +156,7 @@ init 완료 시 다음이 자동으로 실행됩니다:
156
156
 
157
157
  ## 8. 배포
158
158
 
159
- 배포는 관리도구가 이 레포의 GitHub Actions 워크플로우를 `workflow_dispatch`로 트리거하여 수행합니다. SDK는 워크플로우 파일을 생성·관리하지 않으며, 워크플로우의 구성·권한 세팅·CDN 업로드 경로 결정은 모두 관리도구 측에서 이루어집니다.
159
+ 배포는 관리도구가 이 레포의 GitHub Actions 워크플로우를 `workflow_dispatch`로 트리거해서 수행합니다. SDK는 워크플로우 파일을 생성·관리하지 않습니다. 워크플로우의 구성·권한 세팅·CDN 업로드 경로 결정은 모두 관리도구가 맡습니다.
160
160
 
161
161
  ---
162
162
 
package/docs/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
 
3
3
  ## 1. 개요
4
4
 
5
- bstage Template SDK는 서드파티 개발 파트너가 React 등 모던 프레임워크로 템플릿을 작성하면, 이를 Web Component로 변환하여 bstage 플랫폼에 통합하는 개발 도구입니다.
5
+ bstage Template SDK는 서드파티 개발 파트너가 React 등 모던 프레임워크로 작성한 템플릿을 Web Component로 변환해 bstage 플랫폼에 통합하는 개발 도구입니다.
6
6
 
7
- 서드파티는 Web Component를 직접 다루지 않습니다. 평소 프레임워크 개발과 동일한 방식으로 컴포넌트를 작성하고, SDK가 나머지를 처리합니다.
7
+ 서드파티는 Web Component를 직접 다루지 않습니다. 평소 프레임워크 개발과 동일한 방식으로 컴포넌트를 작성하고 SDK가 나머지를 처리합니다.
8
8
 
9
9
  **설계 목표:**
10
10
 
11
11
  - 서드파티 개발자가 플랫폼 내부 구현을 몰라도 템플릿을 개발할 수 있을 것
12
- - 프레임워크 선택이 SDK에 의해 제한되지 않을 것
12
+ - SDK가 프레임워크 선택을 제한하지 않을 것
13
13
  - 서드파티 코드가 플랫폼의 스타일이나 동작을 오염시키지 않을 것
14
14
  - 빌드 산출물이 단일 JS 파일로 완결되어 배포와 로드가 단순할 것
15
15
 
@@ -19,11 +19,11 @@ bstage Template SDK는 서드파티 개발 파트너가 React 등 모던 프레
19
19
 
20
20
  ### 프레임워크 독립성
21
21
 
22
- Core는 특정 프레임워크에 의존하지 않습니다. `FrameworkAdapter` 인터페이스를 정의하고, 각 프레임워크 바인딩(React, Vue 등)이 이를 구현합니다. 새 프레임워크를 지원할 때 Core를 수정할 필요가 없습니다.
22
+ Core는 특정 프레임워크에 의존하지 않습니다. `FrameworkAdapter` 인터페이스를 정의하고 각 프레임워크 바인딩(React, Vue 등)이 이를 구현합니다. 새 프레임워크를 지원할 때 Core를 수정할 필요가 없습니다.
23
23
 
24
24
  ### 스타일 격리
25
25
 
26
- Shadow DOM을 사용하여 서드파티 CSS가 플랫폼 UI에 영향을 주거나, 플랫폼 CSS가 템플릿을 깨뜨리는 것을 방지합니다. CSS는 빌드 시 JS 번들에 인라인되어 Shadow DOM 내부에서만 적용됩니다.
26
+ Shadow DOM으로 서드파티 CSS가 플랫폼 UI에 영향을 주거나 플랫폼 CSS가 템플릿을 깨뜨리는 것을 방지합니다. CSS는 빌드 시 JS 번들에 인라인되어 Shadow DOM 내부에서만 적용됩니다.
27
27
 
28
28
  ### 단일 번들 출력
29
29
 
@@ -31,11 +31,11 @@ Shadow DOM을 사용하여 서드파티 CSS가 플랫폼 UI에 영향을 주거
31
31
 
32
32
  ### 명시적 통신 경계
33
33
 
34
- 서드파티 코드와 플랫폼 사이의 모든 통신은 PlatformBridge를 통한 CustomEvent로 이루어집니다. 직접적인 DOM 접근이나 전역 상태 공유 없이, 정의된 이벤트 인터페이스를 통해서만 상호작용합니다.
34
+ 서드파티 코드와 플랫폼 사이의 모든 통신은 PlatformBridge의 CustomEvent로 오갑니다. DOM 직접 접근이나 전역 상태 공유 없이 정의된 이벤트 인터페이스로만 상호작용합니다.
35
35
 
36
36
  ### 토큰 비노출
37
37
 
38
- 서드파티 코드에서 플랫폼 사용자의 인증 토큰에 직접 접근하지 않습니다. API 호출은 BstageClient를 통해 `appId`/`appSecret` 기반으로 이루어집니다.
38
+ 서드파티 코드는 플랫폼 사용자의 인증 토큰에 직접 접근하지 않습니다. API 호출은 BstageClient가 `appId`/`appKey`로 처리합니다.
39
39
 
40
40
  ---
41
41
 
@@ -50,9 +50,9 @@ Shadow DOM을 사용하여 서드파티 CSS가 플랫폼 UI에 영향을 주거
50
50
  | 양방향 통신 | CustomEvent (동기) | postMessage (비동기) | 직접 호출 |
51
51
  | 제출물 | JS 번들 1개 | HTML + JS (독립 앱) | 빌드 설정 맞춤 필요 |
52
52
 
53
- 1. **기존 Liquid 파이프라인과 가장 유사** — `.liquid` 로드 → 파싱 → HTML 삽입이 `.js` 로드 → 태그 생성 → DOM 추가로 자연스럽게 대체됩니다.
53
+ 1. **기존 Liquid 파이프라인과 가장 유사** — `.liquid` 로드 → 파싱 → HTML 삽입이 `.js` 로드 → 태그 생성 → DOM 추가로 대체됩니다.
54
54
  2. **브라우저 네이티브 표준** — Custom Elements + Shadow DOM은 W3C 표준이며 모든 모던 브라우저에서 별도 라이브러리 없이 동작합니다.
55
- 3. **Shadow DOM 스타일 격리** — 서드파티 CSS가 플랫폼 UI에 영향을 주거나 그 반대 상황을 자연스럽게 방지합니다.
55
+ 3. **Shadow DOM 스타일 격리** — 서드파티 CSS가 플랫폼 UI에 영향을 주는 상황과 그 반대 상황을 방지합니다.
56
56
 
57
57
  ---
58
58
 
@@ -94,9 +94,9 @@ graph LR
94
94
  | **빌드 도구** | cli | 서드파티 | Vite 기반 IIFE 번들 생성 + 산출물 경로 배치, 로컬 개발 서버 |
95
95
  | **디자인 토큰** | design | 서드파티 | 색·타이포·그림자 토큰 (`./user`·`./admin` 서브패스 + css 산출물) |
96
96
 
97
- Core는 프레임워크 바인딩과 플랫폼 통합 양쪽에서 사용되지만, 서드파티에게 직접 노출되지 않습니다. 서드파티는 React 바인딩의 API만 사용하고, 플랫폼은 Host SDK의 API만 사용합니다.
97
+ Core는 프레임워크 바인딩과 플랫폼 통합 양쪽에서 사용되지만 서드파티에게 직접 노출되지 않습니다. 서드파티는 React 바인딩의 API만 쓰고 플랫폼은 Host SDK의 API만 사용합니다.
98
98
 
99
- **어드민 템플릿도 같은 패키지·같은 명령을 씁니다.** 갈리는 것은 템플릿이 선언하는 `target`뿐이며, 런타임 등록 경로와 어댑터는 하나입니다. 다만 **어드민 API 호출 경로는 아직 없습니다** — `BstageClient`는 게이트웨이(유저단)만 지원하고, 어드민용 게이트웨이는 열리지 않았습니다. 자세한 내용은 [GETTING_STARTED.md](./GETTING_STARTED.md)의 "어드민 템플릿" 절을 참조하세요.
99
+ **어드민 템플릿도 같은 패키지·같은 명령을 씁니다.** 갈리는 것은 프로젝트가 `package.json`의 `bstage.target`으로 선언하는 대상뿐이며 런타임 등록 경로와 어댑터는 하나입니다. 다만 **어드민 API 호출 경로는 아직 없습니다** — `BstageClient`는 게이트웨이(유저단)만 지원하고 어드민용 게이트웨이는 열리지 않았습니다. 자세한 내용은 [GETTING_STARTED.md](./GETTING_STARTED.md)의 "어드민 템플릿" 절을 참조하세요.
100
100
 
101
101
  ---
102
102
 
@@ -104,11 +104,11 @@ Core는 프레임워크 바인딩과 플랫폼 통합 양쪽에서 사용되지
104
104
 
105
105
  ### 5.1 FrameworkAdapter
106
106
 
107
- 프레임워크별 렌더링 로직을 캡슐화하는 인터페이스입니다. Core는 이 인터페이스에만 의존하므로, 새 프레임워크 지원 시 Core 수정 없이 어댑터만 추가하면 됩니다.
107
+ 프레임워크별 렌더링 로직을 캡슐화하는 인터페이스입니다. Core는 이 인터페이스에만 의존하므로 새 프레임워크 지원 시 Core 수정 없이 어댑터만 추가하면 됩니다.
108
108
 
109
- **책임:** Shadow DOM 내에 프레임워크 루트를 생성하고, props 갱신과 정리를 처리합니다.
109
+ **책임:** Shadow DOM 내에 프레임워크 루트를 생성하고 props 갱신과 정리를 처리합니다.
110
110
 
111
- **확장 포인트:** 현재 React 어댑터가 구현되어 있으며, 동일 인터페이스로 Vue, Svelte, Vanilla JS 어댑터를 추가할 수 있습니다.
111
+ **확장 포인트:** 현재 React 어댑터가 구현되어 있으며 동일 인터페이스로 Vue, Svelte, Vanilla JS 어댑터를 추가할 수 있습니다.
112
112
 
113
113
  > API 시그니처는 [API_REFERENCE.md](./API_REFERENCE.md#11-frameworkadapter)를 참고하세요.
114
114
 
@@ -122,7 +122,7 @@ FrameworkAdapter를 받아 Web Component(BstageElement)를 생성하는 팩토
122
122
  - `connectedCallback`에서 어댑터와 브릿지 초기화, 렌더링 시작
123
123
  - `disconnectedCallback`에서 어댑터/브릿지 정리 및 참조 해제
124
124
 
125
- Custom Element 등록(`customElements.define`)은 BstageElement 자체가 하지 않고, 프레임워크 바인딩의 `registerTemplate()`이 처리합니다. `createTemplate()`은 메타데이터 첨부와 레지스트리 등록만 수행하며, 빌드 시 CLI의 `registerPlugin`이 `registerTemplate()` 호출 코드를 자동 주입합니다.
125
+ Custom Element 등록(`customElements.define`)은 BstageElement 자체가 하지 않고 프레임워크 바인딩의 `registerTemplate()`이 처리합니다. `createTemplate()`은 메타데이터 첨부와 레지스트리 등록만 수행하며 빌드 시 CLI의 `registerPlugin`이 `registerTemplate()` 호출 코드를 자동 주입합니다.
126
126
 
127
127
  > API 시그니처는 [API_REFERENCE.md](./API_REFERENCE.md#12-createwebcomponent)를 참고하세요.
128
128
 
@@ -143,13 +143,13 @@ Custom Element 등록(`customElements.define`)은 BstageElement 자체가 하지
143
143
 
144
144
  ### 5.4 BstageClient
145
145
 
146
- 파트너에게 공개된 API 접근을 제공하는 HTTP 클라이언트입니다.
146
+ 파트너에게 공개된 API에 접근하는 HTTP 클라이언트입니다.
147
147
 
148
- **설계 의도:** 서드파티가 플랫폼 API를 호출할 때, 플랫폼 사용자의 인증 토큰 대신 `appId`/`appSecret`/`tenantId`를 사용합니다. 이로써 서드파티 코드가 사용자 토큰에 접근할 수 없으며, API 접근 범위를 앱 단위로 제어할 수 있습니다.
148
+ **설계 의도:** 서드파티가 플랫폼 API를 호출할 때, 플랫폼 사용자의 인증 토큰 대신 `appId`/`appKey`/`tenantId`를 사용합니다. 이로써 서드파티 코드가 사용자 토큰에 접근할 수 없으며 API 접근 범위를 앱 단위로 제어할 수 있습니다.
149
149
 
150
- **base URL 결정:** `resolveBaseUrl()` 함수가 `location.origin/gw`를 반환하여 현재 호스트 기반으로 요청합니다. 로컬 개발 시에는 devVitePlugin이 이 함수를 치환하여 localhost 프록시로 라우팅합니다.
150
+ **base URL 결정:** `resolveBaseUrl()` 함수가 `location.origin/gw`를 반환하여 현재 호스트 기반으로 요청합니다. 로컬 개발 시에는 devVitePlugin이 이 함수를 치환해 localhost 프록시로 라우팅합니다.
151
151
 
152
- **커스텀 fetch 주입:** `globalThis.__bstage_fetch__`를 통해 플랫폼이 인증 헤더(Authorization, CF Access)를 포함하는 fetch를 주입할 수 있습니다. 이를 통해 템플릿이 플랫폼과 동일한 인증 컨텍스트로 API를 호출할 수 있습니다.
152
+ **커스텀 fetch 주입:** 플랫폼은 `globalThis.__bstage_fetch__`를 통해 인증 헤더(Authorization, CF Access)를 포함하는 fetch를 주입할 수 있습니다. 그러면 템플릿이 플랫폼과 동일한 인증 컨텍스트로 API를 호출합니다.
153
153
 
154
154
  > API 시그니처와 사용 예시는 [API_REFERENCE.md](./API_REFERENCE.md#14-bstageclient)를 참고하세요.
155
155
 
@@ -204,7 +204,7 @@ flowchart TD
204
204
  J --> K["6. 버퍼링된 dispatch 리플레이"]
205
205
  ```
206
206
 
207
- **dispatch 버퍼링:** `tpl.dispatch()`를 mount 직후에 호출하면 React 렌더링이 아직 완료되지 않아 이벤트가 유실될 수 있다. TemplateHandle은 템플릿이 `bstage:__ready__` 이벤트를 발행할 때까지 dispatch를 버퍼링하고, ready 후 순서대로 리플레이한다.
207
+ **dispatch 버퍼링:** `tpl.dispatch()`를 mount 직후에 호출하면 React 렌더링이 아직 완료되지 않아 이벤트가 유실될 수 있습니다. TemplateHandle은 템플릿이 `bstage:__ready__` 이벤트를 발행할 때까지 dispatch를 버퍼링하고 ready 후 순서대로 리플레이합니다.
208
208
 
209
209
  ### 런타임 — 언마운트
210
210
 
@@ -242,7 +242,7 @@ Shadow DOM은 **스타일 격리**이지 **보안 격리**가 아닙니다. Java
242
242
  | 토큰/쿠키 탈취 | 민감 정보는 HttpOnly 쿠키로만 관리 |
243
243
  | API 남용 | API Gateway에서 rate limiting |
244
244
 
245
- 서드파티의 API 접근은 BstageClient를 통해 `appId`/`appSecret` 기반으로 이루어지며, 플랫폼 사용자의 인증 토큰에 직접 접근하지 않습니다.
245
+ 서드파티는 BstageClient를 통해 `appId`/`appKey`로 API에 접근하고 플랫폼 사용자의 인증 토큰에는 직접 접근하지 않습니다.
246
246
 
247
247
  ---
248
248
 
@@ -132,7 +132,7 @@ import { createTemplate, usePlatformEvent } from '@bstage-sdk/react'
132
132
  import { BstageClient } from '@bstage-sdk/core'
133
133
  import { useState } from 'react'
134
134
 
135
- const client = new BstageClient({ appId: '...', appSecret: '...', tenantId: '...' })
135
+ const client = new BstageClient({ appId: '...', appKey: '...', tenantId: '...' })
136
136
 
137
137
  function TicketCustomField() {
138
138
  const [resourceId, setResourceId] = useState<string>()
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@bstage-sdk/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "bstage SDK core — HTTP client for b.stage API",
5
5
  "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "https://github.com/bstage-sdk/web-sdk.git",
@@ -32,9 +35,6 @@
32
35
  "typescript": "^5.5.0"
33
36
  },
34
37
  "license": "MIT",
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
38
  "scripts": {
39
39
  "build": "tsup && rm -rf docs && cp -r ../../docs docs",
40
40
  "typecheck": "tsc --noEmit"