@bstage-sdk/bridge 0.10.0 → 0.11.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/README.md CHANGED
@@ -219,7 +219,7 @@ external에 인증을 붙이는 fetch를 주입하지 않는다. 외부 갈래
219
219
 
220
220
  ## 프레임 문서
221
221
 
222
- `createSrcdoc`가 만드는 문서는 CSP로 프레임의 직접 네트워크를 좁힌다. 스크립트는 nonce가 붙은
222
+ `createSrcdoc`가 만드는 문서는 CSP로 프레임이 불러올 수 있는 것을 좁힌다. 스크립트는 nonce가 붙은
223
223
  부트스트랩과 `assetOrigin`의 번들만 실행된다. 부트스트랩은 브리지 클라이언트를 설치하고 init을 받은 뒤
224
224
  엔트리를 로드해 custom element를 붙이고, 문서 높이를 `resize`로 보고한다.
225
225
 
@@ -230,11 +230,12 @@ style-src 'unsafe-inline' {assetOrigin}
230
230
  img-src {assetOrigin} {mediaOrigins…} data: blob:
231
231
  font-src {assetOrigin} data:
232
232
  media-src {assetOrigin} {mediaOrigins…} blob:
233
- connect-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'
233
+ connect-src https: wss:; frame-src 'none'; form-action 'none'; base-uri 'none'
234
234
  ```
235
235
 
236
- - `fetch`·XHR·WebSocket·EventSource는 `connect-src 'none'`으로 막힌다. API 호출은 브리지(`request`)로만 간다 —
237
- `allowExternal: true`면 브리지가 무인증 외부 https 호출을 중개하므로, 프레임이 가진 데이터는 이 경로로도 밖에 나갈 수 있다.
236
+ - `connect-src`는 항상 `https: wss:`다(아래 "프레임 네트워크"). 평문 `http:`·`ws:`는 열지 않는다. 토큰이 필요한 API는
237
+ 부트스트랩의 `__bstage_fetch__`가 브리지로 보내고, 그 밖의 https API는 템플릿이 프레임 안에서 직접 부른다.
238
+ 브리지 fetch(`__bstage_bridge__.fetch`)의 external 갈래(`allowExternal: true`)도 호환을 위해 그대로 남는다.
238
239
  - img-src·media-src에 맨 `https:`는 두지 않는다 — 두면 `<img src="https://임의-origin/?d=…">`가 자유로운 GET
239
240
  유출 통로가 된다. 외부 이미지·미디어가 필요하면 `mediaOrigins`에 **정확한 https origin**만 넘긴다
240
241
  (경로·쿼리·userinfo·와일드카드·대문자 호스트 불가, 최대 20개, 하나라도 틀리면 `TypeError`). font-src는 넓히지 않는다.
@@ -243,6 +244,54 @@ connect-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'
243
244
  절대 들어가지 않는다**는 것이다 — 인증은 호스트의 `fetchImpl`이 붙이고, 토큰을 응답으로 돌려주는 인증 경로는
244
245
  게이트웨이 거절 목록(`gatewayDenyPrefixes`·`gatewayDenySegments`)으로 막는다. 프레임에 넘기는 init·이벤트·응답에 민감 정보를 싣지 않는다.
245
246
 
247
+ ### 프레임 네트워크 (항상 적용)
248
+
249
+ 결정(2026-10-22): 격리 프레임에서 **토큰이 필요한 정해진 API만 브리지**로 보내고(호스트가 팬 토큰을 붙인다),
250
+ **그 밖의 API는 템플릿이 프레임 안에서 직접** 부른다. 호스트 옵션은 없다 — `createSrcdoc`로 만든 모든 프레임(포털 미리보기 포함)이
251
+ 이 규칙으로 돈다.
252
+
253
+ 동작:
254
+
255
+ - CSP `connect-src https: wss:`. `http:`·`ws:`는 열지 않는다. `img-src`·`media-src`·`font-src`는 위처럼 좁게 두고
256
+ `frame-src 'none'`·`form-action 'none'`·`base-uri 'none'`도 그대로다.
257
+ - 부트스트랩이 템플릿 코드보다 먼저 `globalThis.__bstage_fetch__`(쓰기 불가)를 설치한다. inline 모드용으로 쓰인 템플릿이
258
+ 부르는 그 이름이다. `new URL(input, hostOrigin)`으로 정규화한 뒤 갈래를 나눈다.
259
+
260
+ | 목적지 | 갈래 |
261
+ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
262
+ | hostOrigin + 경로가 init `gatewayPrefixes` 안(web-base는 `['/gw/', '/svc/']`) | 브리지 fetch(기존 프로토콜, 호스트가 다시 판정하고 토큰을 붙인다) |
263
+ | origin이 init `partnerOrigins`와 정확히 일치 | 브리지 fetch(`partnerFetchImpl`) |
264
+ | 그 밖의 https | 프레임의 원래 `fetch`(부트스트랩 때 잡은 참조), `credentials: 'omit'`·`referrerPolicy: 'no-referrer'` |
265
+ | hostOrigin의 게이트웨이 밖 경로, https가 아닌 스킴(http·data·blob 등) | `TypeError`(혼동을 막으려고 직접 보내지 않는다) |
266
+
267
+ - init에 `gatewayPrefixes`(호스트 `policy.gatewayPrefixes`)가 실린다. 판정의 정본은 여전히 호스트 정책이고, 프레임 라우터는
268
+ 보낼 길만 고른다. 필드가 없는 옛 호스트면 hostOrigin 요청은 경로와 무관하게 브리지로 보낸다.
269
+ - 라우터는 판정·전송에 쓰는 전역(`fetch`·`URL`과 그 접근자·`String.prototype.slice`·`Promise.prototype.then`·`Object.assign`)과
270
+ init의 라우팅 목록 사본을 부트스트랩 때 잡아 둔다. 템플릿이 나중에 전역·프로토타입·`getInit()` 객체를 바꿔도 갈래는 바뀌지 않는다(테스트로 고정).
271
+ 갈래가 바뀌더라도 토큰은 새지 않는다 — 브리지로 간 요청은 호스트가 따로 판정하고, 직접 간 요청에는 토큰이 없다.
272
+ - 직접 호출에 인증 헤더를 **붙이지 않는다.** 템플릿이 스스로 넣은 헤더는 그대로 간다.
273
+ - `__bstage_bridge__.fetch`(브리지 fetch)와 호스트의 external 갈래(`allowExternal`)는 그대로 동작한다(호환).
274
+ - core `BstageClient`는 프레임 안에서 계속 브리지 fetch를 쓴다 — 기본 목적지가 게이트웨이(`gatewayBase`)라 토큰이 필요한 쪽이다.
275
+ `baseUrl`을 외부로 바꾼 `BstageClient`는 브리지의 external 갈래로 중계되므로 호스트가 `allowExternal: true`여야 한다.
276
+ 외부를 직접 부르려면 `__bstage_fetch__`나 `fetch`를 쓴다(`config.fetch: globalThis.__bstage_fetch__`도 된다).
277
+
278
+ **보안상 맞바꾼 것(정직하게)** — 아래는 이 결정으로 열린 것이다. 끄는 스위치는 없으므로 프레임을 띄우는 모든 호스트가 아래 조건을 갖춘다.
279
+
280
+ - **직접 호출에는 브리지 정책이 적용되지 않는다.** 사설 IP·IP 리터럴·`localhost`·내부 이름 거절, 분당·동시 요청 상한,
281
+ 요청·응답 본문 상한이 없다. 프레임이 가진 데이터는 임의 https로 나갈 수 있고(이전에도 `allowExternal`·자기 내비게이션으로
282
+ 가능했다), 사설망 접근은 브라우저의 Local Network Access에 맡긴다.
283
+ - **요청은 `Origin: null`로 나간다**(sandbox opaque origin). 직접 부를 API는 CORS에서 `*` 또는 `null`을 허용해야 응답을 읽을 수 있다.
284
+ `null`을 credentials와 함께 허용하는 CORS는 다른 sandbox 문서에도 열리는 것이므로 API 쪽에서 피한다.
285
+ - **토큰은 여전히 프레임에 들어가지 않는다.** 팬 토큰·쿠키는 호스트 `fetchImpl`·`partnerFetchImpl`이 브리지 요청에만 붙인다.
286
+ - **같은 origin 게이트웨이·`/svc`는 브리지로만** 간다 — 단 이것은 `__bstage_fetch__` 라우터의 보장이다. 전역 `fetch`·`XMLHttpRequest`·
287
+ `navigator.sendBeacon`·`EventSource`·`WebSocket`은 그대로 남고 CSP `https:`는 hostOrigin도 포함하므로, 템플릿은 hostOrigin에
288
+ 직접(`credentials: 'include'`, `mode: 'no-cors'`, `sendBeacon`은 항상 자격증명 포함) 요청을 보낼 수 있다. 이 요청은 브리지의
289
+ 거절 규칙(`gatewayDenyPrefixes`·`gatewayDenySegments`)·상한을 거치지 않는다. opaque origin에서 나가 cross-site로 취급되므로
290
+ **`SameSite=None` 쿠키만** 실리고, 응답은 호스트 CORS가 `null`을 허용하지 않으면 읽히지 않는다(부작용은 일어날 수 있다).
291
+ 그래서 프레임을 띄우는 호스트는 ① 호스트 origin 쿠키를 `SameSite=Lax`/`Strict`로 두고(`None` 쿠키는 이 직접 요청에 실린다 — 권장 후속 조치)
292
+ ② CORS에 `null` origin을 허용하지 않으며 ③ 쿠키로 인증하는 상태 변경 엔드포인트에 CSRF 방어를 유지한다.
293
+ - 토큰을 응답 본문으로 돌려주는 경로(토큰 갱신 등)는 브리지의 게이트웨이 거절 규칙으로 계속 막힌다 — 직접 호출로는 토큰이 없으니 닿아도 얻을 것이 없다.
294
+
246
295
  프레임 안 제약: 쿠키·`localStorage`·호스트 DOM에 접근할 수 없다(opaque origin). 번역 사전은 CDN이 아니라
247
296
  호스트가 init·`locale` 이벤트의 `translations`로 넘긴다. 브리지 모드의 `fetchTranslations`는 **현재 locale 사전만**
248
297
  돌려주고, 다른 locale(폴백 체인의 `en`·`ko` 등)은 빈 사전 `{}`이다. 같은 locale에 사전만 새로 와도 `useBstageTranslations`가 다시 읽는다.
@@ -90,6 +90,126 @@ function createBridgeFetch(client) {
90
90
  return bridgeFetch;
91
91
  }
92
92
 
93
+ // src/frame/directFetch.ts
94
+ function installDirectFetch(g, client, config) {
95
+ const hostOrigin = config.hostOrigin;
96
+ const callFn = Function.prototype.call;
97
+ const NativeURL = g.URL;
98
+ const rawFetch = g.fetch;
99
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
100
+ const bridgeFetch = client.fetch;
101
+ const strSlice = callFn.bind(String.prototype.slice);
102
+ const promiseThen = callFn.bind(Promise.prototype.then);
103
+ const promiseReject = Promise.reject.bind(Promise);
104
+ const assign = Object.assign;
105
+ const TypeErr = TypeError;
106
+ function getter(ctor, name) {
107
+ if (typeof ctor !== "function") return void 0;
108
+ let proto = ctor.prototype;
109
+ while (proto) {
110
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
111
+ if (desc && typeof desc.get === "function") {
112
+ return callFn.bind(desc.get);
113
+ }
114
+ proto = Object.getPrototypeOf(proto);
115
+ }
116
+ return void 0;
117
+ }
118
+ const getHref = getter(NativeURL, "href");
119
+ const getOrigin = getter(NativeURL, "origin");
120
+ const getProtocol = getter(NativeURL, "protocol");
121
+ const getPathname = getter(NativeURL, "pathname");
122
+ const getRequestUrl = getter(g.Request, "url");
123
+ let gatewayPrefixes = null;
124
+ let partnerOrigins = [];
125
+ let routesReady = false;
126
+ function copyStrings(value) {
127
+ if (!Array.isArray(value)) return null;
128
+ const out = [];
129
+ for (let i = 0; i < value.length; i += 1) {
130
+ if (typeof value[i] === "string") out[out.length] = value[i];
131
+ }
132
+ return out;
133
+ }
134
+ function takeSnapshot(init) {
135
+ if (routesReady) return;
136
+ const raw = init;
137
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
138
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
139
+ routesReady = true;
140
+ }
141
+ promiseThen(client.ready, takeSnapshot);
142
+ function matchesPrefix(pathname, prefix) {
143
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
144
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
145
+ }
146
+ function routeOf(origin, protocol, pathname) {
147
+ if (origin === hostOrigin) {
148
+ if (gatewayPrefixes === null) return "bridge";
149
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
150
+ if (matchesPrefix(pathname, gatewayPrefixes[i])) return "bridge";
151
+ }
152
+ return "reject";
153
+ }
154
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
155
+ if (origin === partnerOrigins[i]) return "bridge";
156
+ }
157
+ return protocol === "https:" ? "direct" : "reject";
158
+ }
159
+ function requestUrlOf(input) {
160
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
161
+ try {
162
+ return getRequestUrl(input);
163
+ } catch {
164
+ return void 0;
165
+ }
166
+ }
167
+ function dispatch(input, init) {
168
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
169
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
170
+ }
171
+ const requestUrl = requestUrlOf(input);
172
+ const request = requestUrl !== void 0;
173
+ let parsed;
174
+ try {
175
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
176
+ } catch {
177
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
178
+ }
179
+ const href = getHref(parsed);
180
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
181
+ const target = request ? input : href;
182
+ if (route === "bridge") return bridgeFetch(target, init);
183
+ if (route === "reject") {
184
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
185
+ }
186
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
187
+ return nativeFetch(
188
+ g,
189
+ target,
190
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
191
+ );
192
+ }
193
+ function bstageFetch(input, init) {
194
+ try {
195
+ if (routesReady) return dispatch(input, init);
196
+ return promiseThen(client.ready, function(value) {
197
+ takeSnapshot(value);
198
+ return dispatch(input, init);
199
+ });
200
+ } catch {
201
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
202
+ }
203
+ }
204
+ Object.defineProperty(g, "__bstage_fetch__", {
205
+ value: bstageFetch,
206
+ writable: false,
207
+ configurable: false,
208
+ enumerable: false
209
+ });
210
+ return bstageFetch;
211
+ }
212
+
93
213
  // src/frame/runtime.ts
94
214
  function bridgeRuntime(g, makeFetch, config) {
95
215
  const NS = "bstage-bridge";
@@ -342,5 +462,6 @@ function bridgeRuntime(g, makeFetch, config) {
342
462
 
343
463
  export {
344
464
  bridgeRuntime,
345
- createBridgeFetch
465
+ createBridgeFetch,
466
+ installDirectFetch
346
467
  };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  bridgeRuntime,
3
- createBridgeFetch
4
- } from "./chunk-N3JID7KQ.js";
3
+ createBridgeFetch,
4
+ installDirectFetch
5
+ } from "./chunk-CSAW3L7P.js";
5
6
 
6
7
  // src/protocol/envelope.ts
7
8
  var BRIDGE_VERSION = 1;
@@ -449,7 +450,7 @@ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
449
450
  `img-src ${media} data: blob:`,
450
451
  `font-src ${assetOrigin} data:`,
451
452
  `media-src ${media} blob:`,
452
- "connect-src 'none'",
453
+ "connect-src https: wss:",
453
454
  "frame-src 'none'",
454
455
  "form-action 'none'",
455
456
  "base-uri 'none'"
@@ -466,6 +467,9 @@ function bootstrapScript(o) {
466
467
  `var bridgeRuntime = ${bridgeRuntime.toString()};`,
467
468
  `var createBridgeFetch = ${createBridgeFetch.toString()};`,
468
469
  "var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
470
+ // 라우터는 항상 심는다. ready.then이 엔트리 로드(아래 client.ready.then)보다 먼저 등록돼야 템플릿보다 먼저 스냅샷을 잡는다.
471
+ `var installDirectFetch = ${installDirectFetch.toString()};`,
472
+ "installDirectFetch(globalThis, client, { hostOrigin: HOST_ORIGIN });",
469
473
  "var lastHeight = -1;",
470
474
  "var scheduled = false;",
471
475
  "function reportHeight() {",
@@ -33,6 +33,9 @@ declare const MAX_ELEMENT_NAME_LENGTH = 100;
33
33
  /**
34
34
  * 브리지 프로토콜 CSP. 값은 이스케이프 전 원문(호출자가 속성에 넣을 때 이스케이프).
35
35
  *
36
+ * connect-src는 항상 `https: wss:`다(프레임 네트워크 규칙, README "프레임 네트워크") — 토큰이 필요한 API는
37
+ * 부트스트랩의 `__bstage_fetch__`가 브리지로 보내고, 그 밖의 https API는 템플릿이 직접 부른다. 평문(`http:`·`ws:`)은 열지 않는다.
38
+ *
36
39
  * img-src·media-src에 맨 `https:`를 두지 않는다 — 두면 템플릿이 `<img src="https://attacker/?d=...">`로
37
40
  * 임의 origin에 GET을 보낼 수 있다. 외부 이미지·미디어는 `mediaOrigins`로 정확한 origin만 연다.
38
41
  *
@@ -33,6 +33,9 @@ declare const MAX_ELEMENT_NAME_LENGTH = 100;
33
33
  /**
34
34
  * 브리지 프로토콜 CSP. 값은 이스케이프 전 원문(호출자가 속성에 넣을 때 이스케이프).
35
35
  *
36
+ * connect-src는 항상 `https: wss:`다(프레임 네트워크 규칙, README "프레임 네트워크") — 토큰이 필요한 API는
37
+ * 부트스트랩의 `__bstage_fetch__`가 브리지로 보내고, 그 밖의 https API는 템플릿이 직접 부른다. 평문(`http:`·`ws:`)은 열지 않는다.
38
+ *
36
39
  * img-src·media-src에 맨 `https:`를 두지 않는다 — 두면 템플릿이 `<img src="https://attacker/?d=...">`로
37
40
  * 임의 origin에 GET을 보낼 수 있다. 외부 이미지·미디어는 `mediaOrigins`로 정확한 origin만 연다.
38
41
  *
package/dist/frame.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var frame_exports = {};
22
22
  __export(frame_exports, {
23
23
  createBridgeClient: () => createBridgeClient,
24
- createBridgeFetch: () => createBridgeFetch
24
+ createBridgeFetch: () => createBridgeFetch,
25
+ installDirectFetch: () => installDirectFetch
25
26
  });
26
27
  module.exports = __toCommonJS(frame_exports);
27
28
 
@@ -379,8 +380,129 @@ function createBridgeClient(options = {}) {
379
380
  }
380
381
  return bridgeRuntime(g, createBridgeFetch, { session, hostOrigin });
381
382
  }
383
+
384
+ // src/frame/directFetch.ts
385
+ function installDirectFetch(g, client, config) {
386
+ const hostOrigin = config.hostOrigin;
387
+ const callFn = Function.prototype.call;
388
+ const NativeURL = g.URL;
389
+ const rawFetch = g.fetch;
390
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
391
+ const bridgeFetch = client.fetch;
392
+ const strSlice = callFn.bind(String.prototype.slice);
393
+ const promiseThen = callFn.bind(Promise.prototype.then);
394
+ const promiseReject = Promise.reject.bind(Promise);
395
+ const assign = Object.assign;
396
+ const TypeErr = TypeError;
397
+ function getter(ctor, name) {
398
+ if (typeof ctor !== "function") return void 0;
399
+ let proto = ctor.prototype;
400
+ while (proto) {
401
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
402
+ if (desc && typeof desc.get === "function") {
403
+ return callFn.bind(desc.get);
404
+ }
405
+ proto = Object.getPrototypeOf(proto);
406
+ }
407
+ return void 0;
408
+ }
409
+ const getHref = getter(NativeURL, "href");
410
+ const getOrigin = getter(NativeURL, "origin");
411
+ const getProtocol = getter(NativeURL, "protocol");
412
+ const getPathname = getter(NativeURL, "pathname");
413
+ const getRequestUrl = getter(g.Request, "url");
414
+ let gatewayPrefixes = null;
415
+ let partnerOrigins = [];
416
+ let routesReady = false;
417
+ function copyStrings(value) {
418
+ if (!Array.isArray(value)) return null;
419
+ const out = [];
420
+ for (let i = 0; i < value.length; i += 1) {
421
+ if (typeof value[i] === "string") out[out.length] = value[i];
422
+ }
423
+ return out;
424
+ }
425
+ function takeSnapshot(init) {
426
+ if (routesReady) return;
427
+ const raw = init;
428
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
429
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
430
+ routesReady = true;
431
+ }
432
+ promiseThen(client.ready, takeSnapshot);
433
+ function matchesPrefix(pathname, prefix) {
434
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
435
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
436
+ }
437
+ function routeOf(origin, protocol, pathname) {
438
+ if (origin === hostOrigin) {
439
+ if (gatewayPrefixes === null) return "bridge";
440
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
441
+ if (matchesPrefix(pathname, gatewayPrefixes[i])) return "bridge";
442
+ }
443
+ return "reject";
444
+ }
445
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
446
+ if (origin === partnerOrigins[i]) return "bridge";
447
+ }
448
+ return protocol === "https:" ? "direct" : "reject";
449
+ }
450
+ function requestUrlOf(input) {
451
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
452
+ try {
453
+ return getRequestUrl(input);
454
+ } catch {
455
+ return void 0;
456
+ }
457
+ }
458
+ function dispatch(input, init) {
459
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
460
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
461
+ }
462
+ const requestUrl = requestUrlOf(input);
463
+ const request = requestUrl !== void 0;
464
+ let parsed;
465
+ try {
466
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
467
+ } catch {
468
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
469
+ }
470
+ const href = getHref(parsed);
471
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
472
+ const target = request ? input : href;
473
+ if (route === "bridge") return bridgeFetch(target, init);
474
+ if (route === "reject") {
475
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
476
+ }
477
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
478
+ return nativeFetch(
479
+ g,
480
+ target,
481
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
482
+ );
483
+ }
484
+ function bstageFetch(input, init) {
485
+ try {
486
+ if (routesReady) return dispatch(input, init);
487
+ return promiseThen(client.ready, function(value) {
488
+ takeSnapshot(value);
489
+ return dispatch(input, init);
490
+ });
491
+ } catch {
492
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
493
+ }
494
+ }
495
+ Object.defineProperty(g, "__bstage_fetch__", {
496
+ value: bstageFetch,
497
+ writable: false,
498
+ configurable: false,
499
+ enumerable: false
500
+ });
501
+ return bstageFetch;
502
+ }
382
503
  // Annotate the CommonJS export names for ESM import in node:
383
504
  0 && (module.exports = {
384
505
  createBridgeClient,
385
- createBridgeFetch
506
+ createBridgeFetch,
507
+ installDirectFetch
386
508
  });
package/dist/frame.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-CRGnyf-6.cjs';
1
+ import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-BjpZyiLy.cjs';
2
2
 
3
3
  /** 프레임 런타임 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
4
4
  interface BridgeClientConfig {
@@ -73,4 +73,45 @@ declare function createBridgeClient(options?: CreateBridgeClientOptions): Bridge
73
73
  */
74
74
  declare function createBridgeFetch(client: BridgeRequester): typeof globalThis.fetch;
75
75
 
76
- export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type Unsubscribe, createBridgeClient, createBridgeFetch };
76
+ /** `installDirectFetch` 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
77
+ interface DirectFetchConfig {
78
+ /** 호스트 페이지 origin(srcdoc에 심긴 값). 상대 URL의 기준이자 "같은 origin" 판정 기준. */
79
+ readonly hostOrigin: string;
80
+ }
81
+ /** 프레임 fetch 라우터가 쓰는 전역의 최소 모양 — 테스트에서 가짜 창을 넘길 수 있게 한다. */
82
+ interface DirectFetchGlobal {
83
+ fetch?: typeof globalThis.fetch;
84
+ URL?: typeof URL;
85
+ Request?: typeof Request;
86
+ __bstage_fetch__?: typeof globalThis.fetch;
87
+ }
88
+ /** 갈래 판정 결과. */
89
+ type DirectRoute = 'bridge' | 'direct' | 'reject';
90
+ /**
91
+ * 격리 프레임의 fetch 라우터를(`createSrcdoc` 부트스트랩이 항상 설치한다, README "프레임 네트워크")
92
+ * `globalThis.__bstage_fetch__`에 설치한다. inline 모드용으로 쓰인 템플릿이 부르는 그 이름이다.
93
+ *
94
+ * 갈래(`new URL(input, hostOrigin)`으로 정규화한 뒤):
95
+ * - **브리지**(토큰이 필요한 목적지): hostOrigin과 같은 origin이고 경로가 init `gatewayPrefixes` 안, 또는
96
+ * origin이 init `partnerOrigins`와 정확히 일치. 기존 브리지 fetch(`client.fetch`)로 보내고 호스트가 다시 판정한다.
97
+ * - **직접**: 그 밖의 https. 부트스트랩 때 잡아 둔 프레임의 원래 `fetch`로 `credentials: 'omit'`·
98
+ * `referrerPolicy: 'no-referrer'`를 덮어 부른다. 인증 헤더는 붙이지 않는다(토큰은 프레임에 없다).
99
+ * - **거절**(`TypeError`): hostOrigin의 게이트웨이 밖 경로(혼동 방지 — 직접 보내지 않는다), https가 아닌 스킴.
100
+ *
101
+ * **자기완결 함수다** — `bridgeRuntime`과 함께 srcdoc에 `toString()`으로 심긴다(runtime.ts 주석 참고).
102
+ * 템플릿 코드보다 먼저 돌고, 판정·전송에 쓰는 전역(`fetch`·`URL`과 그 접근자·`String.prototype.slice`·
103
+ * `Promise.prototype.then`·`Object.assign`·`TypeError`)과 init의 라우팅 목록을 이때 잡아 둔다. 그래서 템플릿이
104
+ * 나중에 전역·프로토타입·init 객체를 바꿔도 갈래가 바뀌지 않는다. 갈래가 바뀌어도 토큰은 새지 않는다 —
105
+ * 브리지로 간 요청은 호스트가 따로 판정하고, 직접 간 요청에는 토큰이 없다. 이 고정은 혼동 방지다.
106
+ *
107
+ * init이 `gatewayPrefixes`를 싣지 않으면(옛 호스트) hostOrigin 요청은 경로와 무관하게 브리지로 보낸다
108
+ * (호스트가 판정하므로 안전한 쪽).
109
+ *
110
+ * @param g 프레임 전역(`globalThis`)
111
+ * @param client 프레임 브리지 클라이언트(`bridgeRuntime` 결과, frozen)
112
+ * @param config srcdoc에 심긴 hostOrigin
113
+ * @returns 설치한 fetch 라우터
114
+ */
115
+ declare function installDirectFetch(g: DirectFetchGlobal, client: Pick<BridgeClient, 'fetch' | 'ready'>, config: DirectFetchConfig): typeof globalThis.fetch;
116
+
117
+ export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type DirectFetchConfig, type DirectFetchGlobal, type DirectRoute, type Unsubscribe, createBridgeClient, createBridgeFetch, installDirectFetch };
package/dist/frame.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-CRGnyf-6.js';
1
+ import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-BjpZyiLy.js';
2
2
 
3
3
  /** 프레임 런타임 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
4
4
  interface BridgeClientConfig {
@@ -73,4 +73,45 @@ declare function createBridgeClient(options?: CreateBridgeClientOptions): Bridge
73
73
  */
74
74
  declare function createBridgeFetch(client: BridgeRequester): typeof globalThis.fetch;
75
75
 
76
- export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type Unsubscribe, createBridgeClient, createBridgeFetch };
76
+ /** `installDirectFetch` 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
77
+ interface DirectFetchConfig {
78
+ /** 호스트 페이지 origin(srcdoc에 심긴 값). 상대 URL의 기준이자 "같은 origin" 판정 기준. */
79
+ readonly hostOrigin: string;
80
+ }
81
+ /** 프레임 fetch 라우터가 쓰는 전역의 최소 모양 — 테스트에서 가짜 창을 넘길 수 있게 한다. */
82
+ interface DirectFetchGlobal {
83
+ fetch?: typeof globalThis.fetch;
84
+ URL?: typeof URL;
85
+ Request?: typeof Request;
86
+ __bstage_fetch__?: typeof globalThis.fetch;
87
+ }
88
+ /** 갈래 판정 결과. */
89
+ type DirectRoute = 'bridge' | 'direct' | 'reject';
90
+ /**
91
+ * 격리 프레임의 fetch 라우터를(`createSrcdoc` 부트스트랩이 항상 설치한다, README "프레임 네트워크")
92
+ * `globalThis.__bstage_fetch__`에 설치한다. inline 모드용으로 쓰인 템플릿이 부르는 그 이름이다.
93
+ *
94
+ * 갈래(`new URL(input, hostOrigin)`으로 정규화한 뒤):
95
+ * - **브리지**(토큰이 필요한 목적지): hostOrigin과 같은 origin이고 경로가 init `gatewayPrefixes` 안, 또는
96
+ * origin이 init `partnerOrigins`와 정확히 일치. 기존 브리지 fetch(`client.fetch`)로 보내고 호스트가 다시 판정한다.
97
+ * - **직접**: 그 밖의 https. 부트스트랩 때 잡아 둔 프레임의 원래 `fetch`로 `credentials: 'omit'`·
98
+ * `referrerPolicy: 'no-referrer'`를 덮어 부른다. 인증 헤더는 붙이지 않는다(토큰은 프레임에 없다).
99
+ * - **거절**(`TypeError`): hostOrigin의 게이트웨이 밖 경로(혼동 방지 — 직접 보내지 않는다), https가 아닌 스킴.
100
+ *
101
+ * **자기완결 함수다** — `bridgeRuntime`과 함께 srcdoc에 `toString()`으로 심긴다(runtime.ts 주석 참고).
102
+ * 템플릿 코드보다 먼저 돌고, 판정·전송에 쓰는 전역(`fetch`·`URL`과 그 접근자·`String.prototype.slice`·
103
+ * `Promise.prototype.then`·`Object.assign`·`TypeError`)과 init의 라우팅 목록을 이때 잡아 둔다. 그래서 템플릿이
104
+ * 나중에 전역·프로토타입·init 객체를 바꿔도 갈래가 바뀌지 않는다. 갈래가 바뀌어도 토큰은 새지 않는다 —
105
+ * 브리지로 간 요청은 호스트가 따로 판정하고, 직접 간 요청에는 토큰이 없다. 이 고정은 혼동 방지다.
106
+ *
107
+ * init이 `gatewayPrefixes`를 싣지 않으면(옛 호스트) hostOrigin 요청은 경로와 무관하게 브리지로 보낸다
108
+ * (호스트가 판정하므로 안전한 쪽).
109
+ *
110
+ * @param g 프레임 전역(`globalThis`)
111
+ * @param client 프레임 브리지 클라이언트(`bridgeRuntime` 결과, frozen)
112
+ * @param config srcdoc에 심긴 hostOrigin
113
+ * @returns 설치한 fetch 라우터
114
+ */
115
+ declare function installDirectFetch(g: DirectFetchGlobal, client: Pick<BridgeClient, 'fetch' | 'ready'>, config: DirectFetchConfig): typeof globalThis.fetch;
116
+
117
+ export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type DirectFetchConfig, type DirectFetchGlobal, type DirectRoute, type Unsubscribe, createBridgeClient, createBridgeFetch, installDirectFetch };
package/dist/frame.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  bridgeRuntime,
3
- createBridgeFetch
4
- } from "./chunk-N3JID7KQ.js";
3
+ createBridgeFetch,
4
+ installDirectFetch
5
+ } from "./chunk-CSAW3L7P.js";
5
6
 
6
7
  // src/frame/createBridgeClient.ts
7
8
  var BRIDGE_CONFIG_GLOBAL = "__bstage_bridge_config__";
@@ -17,5 +18,6 @@ function createBridgeClient(options = {}) {
17
18
  }
18
19
  export {
19
20
  createBridgeClient,
20
- createBridgeFetch
21
+ createBridgeFetch,
22
+ installDirectFetch
21
23
  };
package/dist/host.cjs CHANGED
@@ -506,6 +506,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
506
506
  spaceId: String(init.spaceId),
507
507
  hostOrigin,
508
508
  gatewayBase: `${hostOrigin}/gw`,
509
+ gatewayPrefixes: policy.gatewayPrefixes,
509
510
  locale: String(init.locale),
510
511
  theme: sanitizeTheme(init.theme),
511
512
  ...slot ? { slot } : {},
@@ -1338,6 +1339,126 @@ function createBridgeFetch(client) {
1338
1339
  return bridgeFetch;
1339
1340
  }
1340
1341
 
1342
+ // src/frame/directFetch.ts
1343
+ function installDirectFetch(g, client, config) {
1344
+ const hostOrigin = config.hostOrigin;
1345
+ const callFn = Function.prototype.call;
1346
+ const NativeURL = g.URL;
1347
+ const rawFetch = g.fetch;
1348
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
1349
+ const bridgeFetch = client.fetch;
1350
+ const strSlice = callFn.bind(String.prototype.slice);
1351
+ const promiseThen = callFn.bind(Promise.prototype.then);
1352
+ const promiseReject = Promise.reject.bind(Promise);
1353
+ const assign = Object.assign;
1354
+ const TypeErr = TypeError;
1355
+ function getter(ctor, name) {
1356
+ if (typeof ctor !== "function") return void 0;
1357
+ let proto = ctor.prototype;
1358
+ while (proto) {
1359
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
1360
+ if (desc && typeof desc.get === "function") {
1361
+ return callFn.bind(desc.get);
1362
+ }
1363
+ proto = Object.getPrototypeOf(proto);
1364
+ }
1365
+ return void 0;
1366
+ }
1367
+ const getHref = getter(NativeURL, "href");
1368
+ const getOrigin = getter(NativeURL, "origin");
1369
+ const getProtocol = getter(NativeURL, "protocol");
1370
+ const getPathname = getter(NativeURL, "pathname");
1371
+ const getRequestUrl = getter(g.Request, "url");
1372
+ let gatewayPrefixes = null;
1373
+ let partnerOrigins = [];
1374
+ let routesReady = false;
1375
+ function copyStrings(value) {
1376
+ if (!Array.isArray(value)) return null;
1377
+ const out = [];
1378
+ for (let i = 0; i < value.length; i += 1) {
1379
+ if (typeof value[i] === "string") out[out.length] = value[i];
1380
+ }
1381
+ return out;
1382
+ }
1383
+ function takeSnapshot(init) {
1384
+ if (routesReady) return;
1385
+ const raw = init;
1386
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
1387
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
1388
+ routesReady = true;
1389
+ }
1390
+ promiseThen(client.ready, takeSnapshot);
1391
+ function matchesPrefix2(pathname, prefix) {
1392
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
1393
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
1394
+ }
1395
+ function routeOf(origin, protocol, pathname) {
1396
+ if (origin === hostOrigin) {
1397
+ if (gatewayPrefixes === null) return "bridge";
1398
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
1399
+ if (matchesPrefix2(pathname, gatewayPrefixes[i])) return "bridge";
1400
+ }
1401
+ return "reject";
1402
+ }
1403
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
1404
+ if (origin === partnerOrigins[i]) return "bridge";
1405
+ }
1406
+ return protocol === "https:" ? "direct" : "reject";
1407
+ }
1408
+ function requestUrlOf(input) {
1409
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
1410
+ try {
1411
+ return getRequestUrl(input);
1412
+ } catch {
1413
+ return void 0;
1414
+ }
1415
+ }
1416
+ function dispatch2(input, init) {
1417
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
1418
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
1419
+ }
1420
+ const requestUrl = requestUrlOf(input);
1421
+ const request = requestUrl !== void 0;
1422
+ let parsed;
1423
+ try {
1424
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
1425
+ } catch {
1426
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
1427
+ }
1428
+ const href = getHref(parsed);
1429
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
1430
+ const target = request ? input : href;
1431
+ if (route === "bridge") return bridgeFetch(target, init);
1432
+ if (route === "reject") {
1433
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
1434
+ }
1435
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
1436
+ return nativeFetch(
1437
+ g,
1438
+ target,
1439
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
1440
+ );
1441
+ }
1442
+ function bstageFetch(input, init) {
1443
+ try {
1444
+ if (routesReady) return dispatch2(input, init);
1445
+ return promiseThen(client.ready, function(value) {
1446
+ takeSnapshot(value);
1447
+ return dispatch2(input, init);
1448
+ });
1449
+ } catch {
1450
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
1451
+ }
1452
+ }
1453
+ Object.defineProperty(g, "__bstage_fetch__", {
1454
+ value: bstageFetch,
1455
+ writable: false,
1456
+ configurable: false,
1457
+ enumerable: false
1458
+ });
1459
+ return bstageFetch;
1460
+ }
1461
+
1341
1462
  // src/srcdoc/escape.ts
1342
1463
  var HTML_ESCAPES = {
1343
1464
  "&": "&amp;",
@@ -1403,7 +1524,7 @@ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
1403
1524
  `img-src ${media} data: blob:`,
1404
1525
  `font-src ${assetOrigin} data:`,
1405
1526
  `media-src ${media} blob:`,
1406
- "connect-src 'none'",
1527
+ "connect-src https: wss:",
1407
1528
  "frame-src 'none'",
1408
1529
  "form-action 'none'",
1409
1530
  "base-uri 'none'"
@@ -1420,6 +1541,9 @@ function bootstrapScript(o) {
1420
1541
  `var bridgeRuntime = ${bridgeRuntime.toString()};`,
1421
1542
  `var createBridgeFetch = ${createBridgeFetch.toString()};`,
1422
1543
  "var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
1544
+ // 라우터는 항상 심는다. ready.then이 엔트리 로드(아래 client.ready.then)보다 먼저 등록돼야 템플릿보다 먼저 스냅샷을 잡는다.
1545
+ `var installDirectFetch = ${installDirectFetch.toString()};`,
1546
+ "installDirectFetch(globalThis, client, { hostOrigin: HOST_ORIGIN });",
1423
1547
  "var lastHeight = -1;",
1424
1548
  "var scheduled = false;",
1425
1549
  "function reportHeight() {",
package/dist/host.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { f as BridgeHostEventType, e as BridgeEventMap, t as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-CRGnyf-6.cjs';
2
- export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, w as isBridgeSurface } from './messages-CRGnyf-6.cjs';
3
- export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-bOoxttPz.cjs';
1
+ import { f as BridgeHostEventType, e as BridgeEventMap, t as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-BjpZyiLy.cjs';
2
+ export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, w as isBridgeSurface } from './messages-BjpZyiLy.cjs';
3
+ export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-DgQyvIjG.cjs';
4
4
 
5
5
  /** 호스트 fetch 정책. */
6
6
  interface BridgePolicy {
package/dist/host.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { f as BridgeHostEventType, e as BridgeEventMap, t as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-CRGnyf-6.js';
2
- export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, w as isBridgeSurface } from './messages-CRGnyf-6.js';
3
- export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-bOoxttPz.js';
1
+ import { f as BridgeHostEventType, e as BridgeEventMap, t as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-BjpZyiLy.js';
2
+ export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, w as isBridgeSurface } from './messages-BjpZyiLy.js';
3
+ export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-DgQyvIjG.js';
4
4
 
5
5
  /** 호스트 fetch 정책. */
6
6
  interface BridgePolicy {
package/dist/host.js CHANGED
@@ -32,8 +32,8 @@ import {
32
32
  prepareFetch,
33
33
  resolveDestination,
34
34
  resolvePolicy
35
- } from "./chunk-JH3A3Z2A.js";
36
- import "./chunk-N3JID7KQ.js";
35
+ } from "./chunk-ND2HFYBP.js";
36
+ import "./chunk-CSAW3L7P.js";
37
37
 
38
38
  // src/host/rateLimiter.ts
39
39
  var DEFAULT_WINDOW_MS = 6e4;
@@ -115,6 +115,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
115
115
  spaceId: String(init.spaceId),
116
116
  hostOrigin,
117
117
  gatewayBase: `${hostOrigin}/gw`,
118
+ gatewayPrefixes: policy.gatewayPrefixes,
118
119
  locale: String(init.locale),
119
120
  theme: sanitizeTheme(init.theme),
120
121
  ...slot ? { slot } : {},
package/dist/index.cjs CHANGED
@@ -496,6 +496,126 @@ function createBridgeFetch(client) {
496
496
  return bridgeFetch;
497
497
  }
498
498
 
499
+ // src/frame/directFetch.ts
500
+ function installDirectFetch(g, client, config) {
501
+ const hostOrigin = config.hostOrigin;
502
+ const callFn = Function.prototype.call;
503
+ const NativeURL = g.URL;
504
+ const rawFetch = g.fetch;
505
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
506
+ const bridgeFetch = client.fetch;
507
+ const strSlice = callFn.bind(String.prototype.slice);
508
+ const promiseThen = callFn.bind(Promise.prototype.then);
509
+ const promiseReject = Promise.reject.bind(Promise);
510
+ const assign = Object.assign;
511
+ const TypeErr = TypeError;
512
+ function getter(ctor, name) {
513
+ if (typeof ctor !== "function") return void 0;
514
+ let proto = ctor.prototype;
515
+ while (proto) {
516
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
517
+ if (desc && typeof desc.get === "function") {
518
+ return callFn.bind(desc.get);
519
+ }
520
+ proto = Object.getPrototypeOf(proto);
521
+ }
522
+ return void 0;
523
+ }
524
+ const getHref = getter(NativeURL, "href");
525
+ const getOrigin = getter(NativeURL, "origin");
526
+ const getProtocol = getter(NativeURL, "protocol");
527
+ const getPathname = getter(NativeURL, "pathname");
528
+ const getRequestUrl = getter(g.Request, "url");
529
+ let gatewayPrefixes = null;
530
+ let partnerOrigins = [];
531
+ let routesReady = false;
532
+ function copyStrings(value) {
533
+ if (!Array.isArray(value)) return null;
534
+ const out = [];
535
+ for (let i = 0; i < value.length; i += 1) {
536
+ if (typeof value[i] === "string") out[out.length] = value[i];
537
+ }
538
+ return out;
539
+ }
540
+ function takeSnapshot(init) {
541
+ if (routesReady) return;
542
+ const raw = init;
543
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
544
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
545
+ routesReady = true;
546
+ }
547
+ promiseThen(client.ready, takeSnapshot);
548
+ function matchesPrefix(pathname, prefix) {
549
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
550
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
551
+ }
552
+ function routeOf(origin, protocol, pathname) {
553
+ if (origin === hostOrigin) {
554
+ if (gatewayPrefixes === null) return "bridge";
555
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
556
+ if (matchesPrefix(pathname, gatewayPrefixes[i])) return "bridge";
557
+ }
558
+ return "reject";
559
+ }
560
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
561
+ if (origin === partnerOrigins[i]) return "bridge";
562
+ }
563
+ return protocol === "https:" ? "direct" : "reject";
564
+ }
565
+ function requestUrlOf(input) {
566
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
567
+ try {
568
+ return getRequestUrl(input);
569
+ } catch {
570
+ return void 0;
571
+ }
572
+ }
573
+ function dispatch(input, init) {
574
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
575
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
576
+ }
577
+ const requestUrl = requestUrlOf(input);
578
+ const request = requestUrl !== void 0;
579
+ let parsed;
580
+ try {
581
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
582
+ } catch {
583
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
584
+ }
585
+ const href = getHref(parsed);
586
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
587
+ const target = request ? input : href;
588
+ if (route === "bridge") return bridgeFetch(target, init);
589
+ if (route === "reject") {
590
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
591
+ }
592
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
593
+ return nativeFetch(
594
+ g,
595
+ target,
596
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
597
+ );
598
+ }
599
+ function bstageFetch(input, init) {
600
+ try {
601
+ if (routesReady) return dispatch(input, init);
602
+ return promiseThen(client.ready, function(value) {
603
+ takeSnapshot(value);
604
+ return dispatch(input, init);
605
+ });
606
+ } catch {
607
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
608
+ }
609
+ }
610
+ Object.defineProperty(g, "__bstage_fetch__", {
611
+ value: bstageFetch,
612
+ writable: false,
613
+ configurable: false,
614
+ enumerable: false
615
+ });
616
+ return bstageFetch;
617
+ }
618
+
499
619
  // src/srcdoc/escape.ts
500
620
  var HTML_ESCAPES = {
501
621
  "&": "&amp;",
@@ -636,7 +756,7 @@ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
636
756
  `img-src ${media} data: blob:`,
637
757
  `font-src ${assetOrigin} data:`,
638
758
  `media-src ${media} blob:`,
639
- "connect-src 'none'",
759
+ "connect-src https: wss:",
640
760
  "frame-src 'none'",
641
761
  "form-action 'none'",
642
762
  "base-uri 'none'"
@@ -653,6 +773,9 @@ function bootstrapScript(o) {
653
773
  `var bridgeRuntime = ${bridgeRuntime.toString()};`,
654
774
  `var createBridgeFetch = ${createBridgeFetch.toString()};`,
655
775
  "var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
776
+ // 라우터는 항상 심는다. ready.then이 엔트리 로드(아래 client.ready.then)보다 먼저 등록돼야 템플릿보다 먼저 스냅샷을 잡는다.
777
+ `var installDirectFetch = ${installDirectFetch.toString()};`,
778
+ "installDirectFetch(globalThis, client, { hostOrigin: HOST_ORIGIN });",
656
779
  "var lastHeight = -1;",
657
780
  "var scheduled = false;",
658
781
  "function reportHeight() {",
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- export { B as BRIDGE_ERROR_CODES, a as BRIDGE_SURFACES, b as BridgeError, c as BridgeErrorCode, d as BridgeErrorLike, e as BridgeEventMap, f as BridgeHostEventType, g as BridgeInit, h as BridgeLimits, i as BridgeRequestMap, j as BridgeRequestType, k as BridgeResult, l as BridgeSurface, m as BridgeTranslations, F as FRAME_UNLOAD_EVENT, n as FetchRequest, o as FetchResult, H as HOST_EVENT_TYPES, I as IFRAME_ATTRIBUTES, p as INIT_RESPONSE, L as LogLevel, M as MemberState, R as READY_EVENT, q as REQUEST_TIMEOUT_MS, r as REQUEST_TYPES, S as SlotState, s as StorageOp, T as ThemeState, t as ToastKind, W as WithSession, u as isBridgeErrorCode, v as isBridgeErrorLike, w as isBridgeSurface, x as isHostEventType, y as isRequestType } from './messages-CRGnyf-6.cjs';
2
- export { C as CreateSrcdocOptions, M as MAX_ELEMENT_NAME_LENGTH, a as MAX_MEDIA_ORIGINS, b as buildFrameCsp, c as createNonce, d as createSession, e as createSrcdoc, i as isValidNonce, f as isValidSession } from './createSrcdoc-bOoxttPz.cjs';
1
+ export { B as BRIDGE_ERROR_CODES, a as BRIDGE_SURFACES, b as BridgeError, c as BridgeErrorCode, d as BridgeErrorLike, e as BridgeEventMap, f as BridgeHostEventType, g as BridgeInit, h as BridgeLimits, i as BridgeRequestMap, j as BridgeRequestType, k as BridgeResult, l as BridgeSurface, m as BridgeTranslations, F as FRAME_UNLOAD_EVENT, n as FetchRequest, o as FetchResult, H as HOST_EVENT_TYPES, I as IFRAME_ATTRIBUTES, p as INIT_RESPONSE, L as LogLevel, M as MemberState, R as READY_EVENT, q as REQUEST_TIMEOUT_MS, r as REQUEST_TYPES, S as SlotState, s as StorageOp, T as ThemeState, t as ToastKind, W as WithSession, u as isBridgeErrorCode, v as isBridgeErrorLike, w as isBridgeSurface, x as isHostEventType, y as isRequestType } from './messages-BjpZyiLy.cjs';
2
+ export { C as CreateSrcdocOptions, M as MAX_ELEMENT_NAME_LENGTH, a as MAX_MEDIA_ORIGINS, b as buildFrameCsp, c as createNonce, d as createSession, e as createSrcdoc, i as isValidNonce, f as isValidSession } from './createSrcdoc-DgQyvIjG.cjs';
3
3
 
4
4
  /** 봉투 버전. 형식이 하위 호환되지 않게 바뀔 때만 올린다. */
5
5
  declare const BRIDGE_VERSION = 1;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { B as BRIDGE_ERROR_CODES, a as BRIDGE_SURFACES, b as BridgeError, c as BridgeErrorCode, d as BridgeErrorLike, e as BridgeEventMap, f as BridgeHostEventType, g as BridgeInit, h as BridgeLimits, i as BridgeRequestMap, j as BridgeRequestType, k as BridgeResult, l as BridgeSurface, m as BridgeTranslations, F as FRAME_UNLOAD_EVENT, n as FetchRequest, o as FetchResult, H as HOST_EVENT_TYPES, I as IFRAME_ATTRIBUTES, p as INIT_RESPONSE, L as LogLevel, M as MemberState, R as READY_EVENT, q as REQUEST_TIMEOUT_MS, r as REQUEST_TYPES, S as SlotState, s as StorageOp, T as ThemeState, t as ToastKind, W as WithSession, u as isBridgeErrorCode, v as isBridgeErrorLike, w as isBridgeSurface, x as isHostEventType, y as isRequestType } from './messages-CRGnyf-6.js';
2
- export { C as CreateSrcdocOptions, M as MAX_ELEMENT_NAME_LENGTH, a as MAX_MEDIA_ORIGINS, b as buildFrameCsp, c as createNonce, d as createSession, e as createSrcdoc, i as isValidNonce, f as isValidSession } from './createSrcdoc-bOoxttPz.js';
1
+ export { B as BRIDGE_ERROR_CODES, a as BRIDGE_SURFACES, b as BridgeError, c as BridgeErrorCode, d as BridgeErrorLike, e as BridgeEventMap, f as BridgeHostEventType, g as BridgeInit, h as BridgeLimits, i as BridgeRequestMap, j as BridgeRequestType, k as BridgeResult, l as BridgeSurface, m as BridgeTranslations, F as FRAME_UNLOAD_EVENT, n as FetchRequest, o as FetchResult, H as HOST_EVENT_TYPES, I as IFRAME_ATTRIBUTES, p as INIT_RESPONSE, L as LogLevel, M as MemberState, R as READY_EVENT, q as REQUEST_TIMEOUT_MS, r as REQUEST_TYPES, S as SlotState, s as StorageOp, T as ThemeState, t as ToastKind, W as WithSession, u as isBridgeErrorCode, v as isBridgeErrorLike, w as isBridgeSurface, x as isHostEventType, y as isRequestType } from './messages-BjpZyiLy.js';
2
+ export { C as CreateSrcdocOptions, M as MAX_ELEMENT_NAME_LENGTH, a as MAX_MEDIA_ORIGINS, b as buildFrameCsp, c as createNonce, d as createSession, e as createSrcdoc, i as isValidNonce, f as isValidSession } from './createSrcdoc-DgQyvIjG.js';
3
3
 
4
4
  /** 봉투 버전. 형식이 하위 호환되지 않게 바뀔 때만 올린다. */
5
5
  declare const BRIDGE_VERSION = 1;
package/dist/index.js CHANGED
@@ -29,8 +29,8 @@ import {
29
29
  isRequestType,
30
30
  isValidNonce,
31
31
  isValidSession
32
- } from "./chunk-JH3A3Z2A.js";
33
- import "./chunk-N3JID7KQ.js";
32
+ } from "./chunk-ND2HFYBP.js";
33
+ import "./chunk-CSAW3L7P.js";
34
34
  export {
35
35
  BRIDGE_ERROR_CODES,
36
36
  BRIDGE_NAMESPACE,
@@ -70,6 +70,12 @@ interface BridgeInit {
70
70
  readonly spaceId: string;
71
71
  readonly hostOrigin: string;
72
72
  readonly gatewayBase: string;
73
+ /**
74
+ * 호스트 정책의 같은 origin 게이트웨이 경로 접두사(`policy.gatewayPrefixes`). 프레임
75
+ * 라우터(`__bstage_fetch__`)가 "브리지로 보낼 같은 origin 경로"를 가르는 데 쓴다. 판정의 정본은 여전히 호스트다.
76
+ * 이 필드가 없는 옛 호스트면 라우터는 hostOrigin 요청을 모두 브리지로 보낸다.
77
+ */
78
+ readonly gatewayPrefixes?: readonly string[];
73
79
  readonly locale: string;
74
80
  readonly theme: ThemeState;
75
81
  readonly slot?: SlotState;
@@ -70,6 +70,12 @@ interface BridgeInit {
70
70
  readonly spaceId: string;
71
71
  readonly hostOrigin: string;
72
72
  readonly gatewayBase: string;
73
+ /**
74
+ * 호스트 정책의 같은 origin 게이트웨이 경로 접두사(`policy.gatewayPrefixes`). 프레임
75
+ * 라우터(`__bstage_fetch__`)가 "브리지로 보낼 같은 origin 경로"를 가르는 데 쓴다. 판정의 정본은 여전히 호스트다.
76
+ * 이 필드가 없는 옛 호스트면 라우터는 hostOrigin 요청을 모두 브리지로 보낸다.
77
+ */
78
+ readonly gatewayPrefixes?: readonly string[];
73
79
  readonly locale: string;
74
80
  readonly theme: ThemeState;
75
81
  readonly slot?: SlotState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bstage-sdk/bridge",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "bstage SDK bridge — isolated template iframe protocol, host policy and frame client",
5
5
  "type": "module",
6
6
  "publishConfig": {