@bstage-sdk/bridge 0.9.1 → 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/dist/host.cjs CHANGED
@@ -107,6 +107,7 @@ var REQUEST_TYPES = [
107
107
  var HOST_EVENT_TYPES = ["slot.context", "locale", "member", "theme", "visibility"];
108
108
  var READY_EVENT = "ready";
109
109
  var INIT_RESPONSE = "init";
110
+ var FRAME_UNLOAD_EVENT = "frame-unload";
110
111
  var BRIDGE_SURFACES = ["page", "slot"];
111
112
  function isBridgeSurface(value) {
112
113
  return BRIDGE_SURFACES.includes(value);
@@ -505,6 +506,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
505
506
  spaceId: String(init.spaceId),
506
507
  hostOrigin,
507
508
  gatewayBase: `${hostOrigin}/gw`,
509
+ gatewayPrefixes: policy.gatewayPrefixes,
508
510
  locale: String(init.locale),
509
511
  theme: sanitizeTheme(init.theme),
510
512
  ...slot ? { slot } : {},
@@ -844,6 +846,9 @@ function assertOptions(options) {
844
846
  "[bstage-bridge] externalFetchImpl\uC5D0 partnerFetchImpl(\uD32C \uD1A0\uD070 fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
845
847
  );
846
848
  }
849
+ if (options.onDispose !== void 0 && typeof options.onDispose !== "function") {
850
+ throw new TypeError("[bstage-bridge] onDispose\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
851
+ }
847
852
  if (!options.iframe || !options.init) {
848
853
  throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
849
854
  }
@@ -866,6 +871,8 @@ function createBridgeHost(options) {
866
871
  let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
867
872
  let inFlightFetches = 0;
868
873
  let disposed = false;
874
+ let boundFrame;
875
+ let pendingVisibility;
869
876
  let resizeHandle;
870
877
  let pendingHeight;
871
878
  const raf = typeof win.requestAnimationFrame === "function" ? (cb) => win.requestAnimationFrame(cb) : (cb) => setTimeout(cb, 16);
@@ -929,13 +936,44 @@ function createBridgeHost(options) {
929
936
  const env = ev.data;
930
937
  if (!isBridgeEnvelope(env)) return;
931
938
  if (!hasSession(env.payload, session)) return;
939
+ const token = env.frame;
940
+ if (!isValidSession(token) || boundFrame !== void 0 && token !== boundFrame) {
941
+ dispose("frame-mismatch");
942
+ return;
943
+ }
944
+ boundFrame = token;
945
+ if (env.kind === "evt" && env.type === FRAME_UNLOAD_EVENT) {
946
+ dispose("frame-unload");
947
+ return;
948
+ }
932
949
  if (env.kind === "evt" && env.type === READY_EVENT) {
933
950
  if (!readyLimiter.take()) return;
934
951
  post(frame, createEnvelope("res", INIT_RESPONSE, env.id, { ok: true, data: current }));
952
+ flushPendingVisibility(frame);
935
953
  return;
936
954
  }
955
+ flushPendingVisibility(frame);
937
956
  if (env.kind === "req") void handleRequest(frame, env);
938
957
  }
958
+ function flushPendingVisibility(frame) {
959
+ if (pendingVisibility === void 0) return;
960
+ const payload = pendingVisibility;
961
+ pendingVisibility = void 0;
962
+ post(frame, createEnvelope("evt", "visibility", createSession(), payload));
963
+ }
964
+ function dispose(reason) {
965
+ if (disposed) return;
966
+ disposed = true;
967
+ win.removeEventListener("message", onMessage);
968
+ if (resizeHandle !== void 0) cancelRaf(resizeHandle);
969
+ resizeHandle = void 0;
970
+ if (options.onDispose) {
971
+ try {
972
+ options.onDispose(reason);
973
+ } catch {
974
+ }
975
+ }
976
+ }
939
977
  win.addEventListener("message", onMessage);
940
978
  return {
941
979
  send(type, payload) {
@@ -946,15 +984,15 @@ function createBridgeHost(options) {
946
984
  }
947
985
  const clean = sanitizeEvent(type, payload);
948
986
  current = applyEvent(current, type, clean);
987
+ if (boundFrame === void 0) {
988
+ if (type === "visibility") pendingVisibility = clean;
989
+ return;
990
+ }
949
991
  const frame = iframe.contentWindow;
950
992
  if (frame) post(frame, createEnvelope("evt", type, createSession(), clean));
951
993
  },
952
994
  dispose() {
953
- if (disposed) return;
954
- disposed = true;
955
- win.removeEventListener("message", onMessage);
956
- if (resizeHandle !== void 0) cancelRaf(resizeHandle);
957
- resizeHandle = void 0;
995
+ dispose("manual");
958
996
  }
959
997
  };
960
998
  }
@@ -972,6 +1010,20 @@ function bridgeRuntime(g, makeFetch, config) {
972
1010
  const session = config.session;
973
1011
  const hostOrigin = config.hostOrigin;
974
1012
  const parent = g.parent;
1013
+ const sendToHost = parent ? parent.postMessage.bind(parent) : void 0;
1014
+ const stopInterval = clearInterval;
1015
+ const stopTimeout = clearTimeout;
1016
+ let readPersisted;
1017
+ const ownDescriptor = Object.getOwnPropertyDescriptor;
1018
+ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
1019
+ try {
1020
+ const pte = g.PageTransitionEvent;
1021
+ const desc = pte ? Object.getOwnPropertyDescriptor(pte.prototype, "persisted") : void 0;
1022
+ if (desc && typeof desc.get === "function")
1023
+ readPersisted = Function.prototype.call.bind(desc.get);
1024
+ } catch {
1025
+ readPersisted = void 0;
1026
+ }
975
1027
  const pending = /* @__PURE__ */ new Map();
976
1028
  const listeners = /* @__PURE__ */ new Map();
977
1029
  let init;
@@ -988,6 +1040,17 @@ function bridgeRuntime(g, makeFetch, config) {
988
1040
  return out;
989
1041
  }
990
1042
  const idPrefix = randomPrefix();
1043
+ function secureToken() {
1044
+ if (!g.crypto || typeof g.crypto.getRandomValues !== "function") return "";
1045
+ const bytes = new Uint8Array(16);
1046
+ g.crypto.getRandomValues(bytes);
1047
+ let out = "";
1048
+ for (let i = 0; i < bytes.length; i += 1) out += (bytes[i] + 256).toString(16).slice(1);
1049
+ return out;
1050
+ }
1051
+ const frameToken = secureToken();
1052
+ let closed = frameToken === "";
1053
+ let unloaded = false;
991
1054
  function nextId() {
992
1055
  seq += 1;
993
1056
  return idPrefix + "-" + seq;
@@ -999,9 +1062,12 @@ function bridgeRuntime(g, makeFetch, config) {
999
1062
  return err;
1000
1063
  }
1001
1064
  function post(kind, type, id, payload) {
1002
- if (!parent) return;
1065
+ if (!sendToHost || closed) return;
1003
1066
  const body = Object.assign({}, payload, { session });
1004
- parent.postMessage({ v: 1, ns: NS, id, kind, type, payload: body }, hostOrigin);
1067
+ sendToHost(
1068
+ { v: 1, ns: NS, id, kind, type, payload: body, frame: frameToken },
1069
+ hostOrigin
1070
+ );
1005
1071
  }
1006
1072
  function isEnvelope(m) {
1007
1073
  if (typeof m !== "object" || m === null) return false;
@@ -1074,6 +1140,54 @@ function bridgeRuntime(g, makeFetch, config) {
1074
1140
  }
1075
1141
  }
1076
1142
  g.addEventListener("message", onMessage);
1143
+ function isPersisted(ev) {
1144
+ try {
1145
+ if (readPersisted) return readPersisted(ev) === true;
1146
+ if (typeof ev !== "object" || ev === null) return false;
1147
+ const desc = ownDescriptor(ev, "persisted");
1148
+ return !!desc && hasOwn(desc, "value") && desc.value === true;
1149
+ } catch {
1150
+ return false;
1151
+ }
1152
+ }
1153
+ function onPageHide(ev) {
1154
+ if (unloaded || isPersisted(ev)) return;
1155
+ unloaded = true;
1156
+ closed = true;
1157
+ if (sendToHost) {
1158
+ try {
1159
+ sendToHost(
1160
+ {
1161
+ v: 1,
1162
+ ns: NS,
1163
+ id: idPrefix + "-unload",
1164
+ kind: "evt",
1165
+ type: "frame-unload",
1166
+ payload: { session },
1167
+ frame: frameToken
1168
+ },
1169
+ hostOrigin
1170
+ );
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ try {
1175
+ stopInterval(readyTimer);
1176
+ } catch {
1177
+ }
1178
+ try {
1179
+ pending.forEach(function(entry) {
1180
+ try {
1181
+ stopTimeout(entry.timer);
1182
+ entry.reject(codeError("UNSUPPORTED", "bridge closed"));
1183
+ } catch {
1184
+ }
1185
+ });
1186
+ pending.clear();
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ g.addEventListener("pagehide", onPageHide, true);
1077
1191
  let readyAttempts = 0;
1078
1192
  function sendReady() {
1079
1193
  readyAttempts += 1;
@@ -1087,6 +1201,10 @@ function bridgeRuntime(g, makeFetch, config) {
1087
1201
  sendReady();
1088
1202
  function request(type, payload) {
1089
1203
  return new Promise(function(resolve, reject) {
1204
+ if (closed) {
1205
+ reject(codeError("UNSUPPORTED", "bridge closed"));
1206
+ return;
1207
+ }
1090
1208
  const id = nextId();
1091
1209
  const timer = setTimeout(function() {
1092
1210
  pending.delete(id);
@@ -1221,6 +1339,126 @@ function createBridgeFetch(client) {
1221
1339
  return bridgeFetch;
1222
1340
  }
1223
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
+
1224
1462
  // src/srcdoc/escape.ts
1225
1463
  var HTML_ESCAPES = {
1226
1464
  "&": "&amp;",
@@ -1286,7 +1524,7 @@ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
1286
1524
  `img-src ${media} data: blob:`,
1287
1525
  `font-src ${assetOrigin} data:`,
1288
1526
  `media-src ${media} blob:`,
1289
- "connect-src 'none'",
1527
+ "connect-src https: wss:",
1290
1528
  "frame-src 'none'",
1291
1529
  "form-action 'none'",
1292
1530
  "base-uri 'none'"
@@ -1303,6 +1541,9 @@ function bootstrapScript(o) {
1303
1541
  `var bridgeRuntime = ${bridgeRuntime.toString()};`,
1304
1542
  `var createBridgeFetch = ${createBridgeFetch.toString()};`,
1305
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 });",
1306
1547
  "var lastHeight = -1;",
1307
1548
  "var scheduled = false;",
1308
1549
  "function reportHeight() {",
package/dist/host.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { f as BridgeHostEventType, e as BridgeEventMap, s as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-9N0GFq5b.cjs';
2
- export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, v as isBridgeSurface } from './messages-9N0GFq5b.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 {
@@ -213,11 +213,25 @@ interface CreateBridgeHostOptions {
213
213
  readonly pinnedHeaders?: Readonly<Record<string, string>>;
214
214
  readonly handlers?: BridgeHostHandlers;
215
215
  readonly policy?: Partial<BridgePolicy>;
216
+ /**
217
+ * 호스트가 dispose될 때 한 번 불린다. `frame-unload`(프레임 문서가 내려감)·`frame-mismatch`(토큰이 없거나 다른
218
+ * 메시지 = 같은 창의 다른 문서)는 호스트가 스스로 끊은 경우, `manual`은 `dispose()` 호출이다. 임베더가 iframe을
219
+ * 바로 걷어 내는 데 쓴다. 이 콜백의 예외는 삼킨다.
220
+ */
221
+ readonly onDispose?: (reason: BridgeDisposeReason) => void;
216
222
  }
223
+ /** `onDispose` 사유. */
224
+ type BridgeDisposeReason = 'frame-unload' | 'frame-mismatch' | 'manual';
217
225
  interface BridgeHost {
218
- /** 프레임에 이벤트를 보낸다. 이후 ready에 답하는 init에도 반영된다. */
226
+ /**
227
+ * 프레임에 이벤트를 보낸다. 이후 ready에 답하는 init에도 반영된다.
228
+ * 프레임 토큰이 묶이기(첫 유효 메시지) 전에는 보내지 않고 init에만 반영한다.
229
+ */
219
230
  send<E extends BridgeHostEventType>(type: E, payload: BridgeEventMap[E]): void;
220
- /** 리스너·대기 중인 resize를 정리한다. 이후 메시지는 무시된다. */
231
+ /**
232
+ * 리스너·대기 중인 resize를 정리한다. 이후 메시지는 무시되고, 진행 중이던 요청의 응답도 프레임에 보내지 않는다.
233
+ * 프레임이 `frame-unload`를 보내거나 다른 프레임 토큰이 오면 호스트가 스스로 부른다.
234
+ */
221
235
  dispose(): void;
222
236
  }
223
237
 
@@ -226,7 +240,11 @@ interface BridgeHost {
226
240
  *
227
241
  * - 발신자 확인: `ev.source === iframe.contentWindow`, origin `"null"`(sandbox 프레임), 봉투 형식,
228
242
  * `payload.session === session`이 모두 맞는 메시지만 처리한다.
229
- * - `evt:ready`에 `res:init`으로 답한다(프레임이 재전송하면 매번 최신 init으로).
243
+ * - 세션당 프레임 문서는 하나다: 처음 받은 유효 메시지의 프레임 토큰(`env.frame`)에 세션을 묶고, 토큰이 없거나
244
+ * 다른 메시지(= 같은 contentWindow에 새로 뜬 문서, 예: 템플릿의 자기 내비게이션)가 오면 즉시 dispose한다.
245
+ * 그래서 두 번째 핸드셰이크는 성립하지 않는다.
246
+ * - `evt:frame-unload`(부트스트랩이 pagehide에서 보낸다)를 받으면 즉시 dispose한다.
247
+ * - `evt:ready`에 `res:init`으로 답한다(같은 토큰의 재전송이면 매번 최신 init으로).
230
248
  * - 요청은 fetch 정책(policy.ts)·요청별 상한(validators.ts)을 적용해 처리기 또는 주입 `fetchImpl`로 넘긴다.
231
249
  *
232
250
  * 토큰은 이 함수가 다루지 않는다 — 인증은 `fetchImpl`(플랫폼의 인증 fetch)이 호스트 쪽에서 붙이고,
@@ -259,4 +277,4 @@ interface PathCheck {
259
277
  */
260
278
  declare function checkPath(pathname: string, options?: PathCheckOptions): PathCheck;
261
279
 
262
- export { ALLOWED_METHODS, ALLOWED_REQUEST_HEADERS, type BridgeHost, type BridgeHostHandlers, type BridgeInitInput, type BridgePolicy, BridgeSurface, type CreateBridgeHostOptions, DEFAULT_GATEWAY_DENY_SEGMENTS, DEFAULT_POLICY, type Destination, type DestinationContext, type DestinationKind, type FrameWindow, HOST_ONLY_HEADERS, type HostFetch, type HostWindow, type IframeLike, MAX_DECODE_PASSES, MAX_PARTNER_ORIGINS, type MessageEventLike, type PathCheck, type PathCheckOptions, type PreparedFetch, checkPath, classifyDestination, createBridgeHost, isAllowedGatewayPath, normalizePartnerOrigins, normalizePinnedHeaders, prepareFetch, resolveDestination, resolvePolicy };
280
+ export { ALLOWED_METHODS, ALLOWED_REQUEST_HEADERS, type BridgeDisposeReason, type BridgeHost, type BridgeHostHandlers, type BridgeInitInput, type BridgePolicy, BridgeSurface, type CreateBridgeHostOptions, DEFAULT_GATEWAY_DENY_SEGMENTS, DEFAULT_POLICY, type Destination, type DestinationContext, type DestinationKind, type FrameWindow, HOST_ONLY_HEADERS, type HostFetch, type HostWindow, type IframeLike, MAX_DECODE_PASSES, MAX_PARTNER_ORIGINS, type MessageEventLike, type PathCheck, type PathCheckOptions, type PreparedFetch, checkPath, classifyDestination, createBridgeHost, isAllowedGatewayPath, normalizePartnerOrigins, normalizePinnedHeaders, prepareFetch, resolveDestination, resolvePolicy };
package/dist/host.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { f as BridgeHostEventType, e as BridgeEventMap, s as ToastKind, L as LogLevel, l as BridgeSurface, T as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations } from './messages-9N0GFq5b.js';
2
- export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, v as isBridgeSurface } from './messages-9N0GFq5b.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 {
@@ -213,11 +213,25 @@ interface CreateBridgeHostOptions {
213
213
  readonly pinnedHeaders?: Readonly<Record<string, string>>;
214
214
  readonly handlers?: BridgeHostHandlers;
215
215
  readonly policy?: Partial<BridgePolicy>;
216
+ /**
217
+ * 호스트가 dispose될 때 한 번 불린다. `frame-unload`(프레임 문서가 내려감)·`frame-mismatch`(토큰이 없거나 다른
218
+ * 메시지 = 같은 창의 다른 문서)는 호스트가 스스로 끊은 경우, `manual`은 `dispose()` 호출이다. 임베더가 iframe을
219
+ * 바로 걷어 내는 데 쓴다. 이 콜백의 예외는 삼킨다.
220
+ */
221
+ readonly onDispose?: (reason: BridgeDisposeReason) => void;
216
222
  }
223
+ /** `onDispose` 사유. */
224
+ type BridgeDisposeReason = 'frame-unload' | 'frame-mismatch' | 'manual';
217
225
  interface BridgeHost {
218
- /** 프레임에 이벤트를 보낸다. 이후 ready에 답하는 init에도 반영된다. */
226
+ /**
227
+ * 프레임에 이벤트를 보낸다. 이후 ready에 답하는 init에도 반영된다.
228
+ * 프레임 토큰이 묶이기(첫 유효 메시지) 전에는 보내지 않고 init에만 반영한다.
229
+ */
219
230
  send<E extends BridgeHostEventType>(type: E, payload: BridgeEventMap[E]): void;
220
- /** 리스너·대기 중인 resize를 정리한다. 이후 메시지는 무시된다. */
231
+ /**
232
+ * 리스너·대기 중인 resize를 정리한다. 이후 메시지는 무시되고, 진행 중이던 요청의 응답도 프레임에 보내지 않는다.
233
+ * 프레임이 `frame-unload`를 보내거나 다른 프레임 토큰이 오면 호스트가 스스로 부른다.
234
+ */
221
235
  dispose(): void;
222
236
  }
223
237
 
@@ -226,7 +240,11 @@ interface BridgeHost {
226
240
  *
227
241
  * - 발신자 확인: `ev.source === iframe.contentWindow`, origin `"null"`(sandbox 프레임), 봉투 형식,
228
242
  * `payload.session === session`이 모두 맞는 메시지만 처리한다.
229
- * - `evt:ready`에 `res:init`으로 답한다(프레임이 재전송하면 매번 최신 init으로).
243
+ * - 세션당 프레임 문서는 하나다: 처음 받은 유효 메시지의 프레임 토큰(`env.frame`)에 세션을 묶고, 토큰이 없거나
244
+ * 다른 메시지(= 같은 contentWindow에 새로 뜬 문서, 예: 템플릿의 자기 내비게이션)가 오면 즉시 dispose한다.
245
+ * 그래서 두 번째 핸드셰이크는 성립하지 않는다.
246
+ * - `evt:frame-unload`(부트스트랩이 pagehide에서 보낸다)를 받으면 즉시 dispose한다.
247
+ * - `evt:ready`에 `res:init`으로 답한다(같은 토큰의 재전송이면 매번 최신 init으로).
230
248
  * - 요청은 fetch 정책(policy.ts)·요청별 상한(validators.ts)을 적용해 처리기 또는 주입 `fetchImpl`로 넘긴다.
231
249
  *
232
250
  * 토큰은 이 함수가 다루지 않는다 — 인증은 `fetchImpl`(플랫폼의 인증 fetch)이 호스트 쪽에서 붙이고,
@@ -259,4 +277,4 @@ interface PathCheck {
259
277
  */
260
278
  declare function checkPath(pathname: string, options?: PathCheckOptions): PathCheck;
261
279
 
262
- export { ALLOWED_METHODS, ALLOWED_REQUEST_HEADERS, type BridgeHost, type BridgeHostHandlers, type BridgeInitInput, type BridgePolicy, BridgeSurface, type CreateBridgeHostOptions, DEFAULT_GATEWAY_DENY_SEGMENTS, DEFAULT_POLICY, type Destination, type DestinationContext, type DestinationKind, type FrameWindow, HOST_ONLY_HEADERS, type HostFetch, type HostWindow, type IframeLike, MAX_DECODE_PASSES, MAX_PARTNER_ORIGINS, type MessageEventLike, type PathCheck, type PathCheckOptions, type PreparedFetch, checkPath, classifyDestination, createBridgeHost, isAllowedGatewayPath, normalizePartnerOrigins, normalizePinnedHeaders, prepareFetch, resolveDestination, resolvePolicy };
280
+ export { ALLOWED_METHODS, ALLOWED_REQUEST_HEADERS, type BridgeDisposeReason, type BridgeHost, type BridgeHostHandlers, type BridgeInitInput, type BridgePolicy, BridgeSurface, type CreateBridgeHostOptions, DEFAULT_GATEWAY_DENY_SEGMENTS, DEFAULT_POLICY, type Destination, type DestinationContext, type DestinationKind, type FrameWindow, HOST_ONLY_HEADERS, type HostFetch, type HostWindow, type IframeLike, MAX_DECODE_PASSES, MAX_PARTNER_ORIGINS, type MessageEventLike, type PathCheck, type PathCheckOptions, type PreparedFetch, checkPath, classifyDestination, createBridgeHost, isAllowedGatewayPath, normalizePartnerOrigins, normalizePinnedHeaders, prepareFetch, resolveDestination, resolvePolicy };
package/dist/host.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  BridgeError,
6
6
  DEFAULT_GATEWAY_DENY_SEGMENTS,
7
7
  DEFAULT_POLICY,
8
+ FRAME_UNLOAD_EVENT,
8
9
  HOST_ONLY_HEADERS,
9
10
  IFRAME_ATTRIBUTES,
10
11
  INIT_RESPONSE,
@@ -31,8 +32,8 @@ import {
31
32
  prepareFetch,
32
33
  resolveDestination,
33
34
  resolvePolicy
34
- } from "./chunk-6LV7HEN4.js";
35
- import "./chunk-KQMJY4ZH.js";
35
+ } from "./chunk-ND2HFYBP.js";
36
+ import "./chunk-CSAW3L7P.js";
36
37
 
37
38
  // src/host/rateLimiter.ts
38
39
  var DEFAULT_WINDOW_MS = 6e4;
@@ -114,6 +115,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
114
115
  spaceId: String(init.spaceId),
115
116
  hostOrigin,
116
117
  gatewayBase: `${hostOrigin}/gw`,
118
+ gatewayPrefixes: policy.gatewayPrefixes,
117
119
  locale: String(init.locale),
118
120
  theme: sanitizeTheme(init.theme),
119
121
  ...slot ? { slot } : {},
@@ -453,6 +455,9 @@ function assertOptions(options) {
453
455
  "[bstage-bridge] externalFetchImpl\uC5D0 partnerFetchImpl(\uD32C \uD1A0\uD070 fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
454
456
  );
455
457
  }
458
+ if (options.onDispose !== void 0 && typeof options.onDispose !== "function") {
459
+ throw new TypeError("[bstage-bridge] onDispose\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
460
+ }
456
461
  if (!options.iframe || !options.init) {
457
462
  throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
458
463
  }
@@ -475,6 +480,8 @@ function createBridgeHost(options) {
475
480
  let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
476
481
  let inFlightFetches = 0;
477
482
  let disposed = false;
483
+ let boundFrame;
484
+ let pendingVisibility;
478
485
  let resizeHandle;
479
486
  let pendingHeight;
480
487
  const raf = typeof win.requestAnimationFrame === "function" ? (cb) => win.requestAnimationFrame(cb) : (cb) => setTimeout(cb, 16);
@@ -538,13 +545,44 @@ function createBridgeHost(options) {
538
545
  const env = ev.data;
539
546
  if (!isBridgeEnvelope(env)) return;
540
547
  if (!hasSession(env.payload, session)) return;
548
+ const token = env.frame;
549
+ if (!isValidSession(token) || boundFrame !== void 0 && token !== boundFrame) {
550
+ dispose("frame-mismatch");
551
+ return;
552
+ }
553
+ boundFrame = token;
554
+ if (env.kind === "evt" && env.type === FRAME_UNLOAD_EVENT) {
555
+ dispose("frame-unload");
556
+ return;
557
+ }
541
558
  if (env.kind === "evt" && env.type === READY_EVENT) {
542
559
  if (!readyLimiter.take()) return;
543
560
  post(frame, createEnvelope("res", INIT_RESPONSE, env.id, { ok: true, data: current }));
561
+ flushPendingVisibility(frame);
544
562
  return;
545
563
  }
564
+ flushPendingVisibility(frame);
546
565
  if (env.kind === "req") void handleRequest(frame, env);
547
566
  }
567
+ function flushPendingVisibility(frame) {
568
+ if (pendingVisibility === void 0) return;
569
+ const payload = pendingVisibility;
570
+ pendingVisibility = void 0;
571
+ post(frame, createEnvelope("evt", "visibility", createSession(), payload));
572
+ }
573
+ function dispose(reason) {
574
+ if (disposed) return;
575
+ disposed = true;
576
+ win.removeEventListener("message", onMessage);
577
+ if (resizeHandle !== void 0) cancelRaf(resizeHandle);
578
+ resizeHandle = void 0;
579
+ if (options.onDispose) {
580
+ try {
581
+ options.onDispose(reason);
582
+ } catch {
583
+ }
584
+ }
585
+ }
548
586
  win.addEventListener("message", onMessage);
549
587
  return {
550
588
  send(type, payload) {
@@ -555,15 +593,15 @@ function createBridgeHost(options) {
555
593
  }
556
594
  const clean = sanitizeEvent(type, payload);
557
595
  current = applyEvent(current, type, clean);
596
+ if (boundFrame === void 0) {
597
+ if (type === "visibility") pendingVisibility = clean;
598
+ return;
599
+ }
558
600
  const frame = iframe.contentWindow;
559
601
  if (frame) post(frame, createEnvelope("evt", type, createSession(), clean));
560
602
  },
561
603
  dispose() {
562
- if (disposed) return;
563
- disposed = true;
564
- win.removeEventListener("message", onMessage);
565
- if (resizeHandle !== void 0) cancelRaf(resizeHandle);
566
- resizeHandle = void 0;
604
+ dispose("manual");
567
605
  }
568
606
  };
569
607
  }