@bstage-sdk/bridge 0.10.0 → 0.12.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
@@ -29,6 +29,7 @@ __export(host_exports, {
29
29
  IFRAME_ATTRIBUTES: () => IFRAME_ATTRIBUTES,
30
30
  MAX_DECODE_PASSES: () => MAX_DECODE_PASSES,
31
31
  MAX_PARTNER_ORIGINS: () => MAX_PARTNER_ORIGINS,
32
+ REPORTS_PER_SESSION: () => REPORTS_PER_SESSION,
32
33
  checkPath: () => checkPath,
33
34
  classifyDestination: () => classifyDestination,
34
35
  createBridgeHost: () => createBridgeHost,
@@ -102,7 +103,8 @@ var REQUEST_TYPES = [
102
103
  "resize",
103
104
  "track",
104
105
  "storage",
105
- "log"
106
+ "log",
107
+ "report"
106
108
  ];
107
109
  var HOST_EVENT_TYPES = ["slot.context", "locale", "member", "theme", "visibility"];
108
110
  var READY_EVENT = "ready";
@@ -112,6 +114,12 @@ var BRIDGE_SURFACES = ["page", "slot"];
112
114
  function isBridgeSurface(value) {
113
115
  return BRIDGE_SURFACES.includes(value);
114
116
  }
117
+ var TEMPLATE_REPORT_KINDS = ["error", "rejection", "load"];
118
+ var REPORT_MESSAGE_MAX_LENGTH = 300;
119
+ var REPORT_MAX_FRAMES = 10;
120
+ var REPORT_FRAME_MAX_LENGTH = 200;
121
+ var REPORT_COUNT_MAX = 100;
122
+ var REPORTS_PER_SESSION = 5;
115
123
  var IFRAME_ATTRIBUTES = {
116
124
  sandbox: "allow-scripts",
117
125
  allow: "autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write",
@@ -506,6 +514,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
506
514
  spaceId: String(init.spaceId),
507
515
  hostOrigin,
508
516
  gatewayBase: `${hostOrigin}/gw`,
517
+ gatewayPrefixes: policy.gatewayPrefixes,
509
518
  locale: String(init.locale),
510
519
  theme: sanitizeTheme(init.theme),
511
520
  ...slot ? { slot } : {},
@@ -668,6 +677,32 @@ function validateLog(payload) {
668
677
  if (!known || typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid log");
669
678
  return { level: known, message: message.slice(0, LOG_MAX_LENGTH) };
670
679
  }
680
+ function framesCopy(frames) {
681
+ if (!Array.isArray(frames) || frames.length > REPORT_MAX_FRAMES) return null;
682
+ const out = [];
683
+ for (let i = 0; i < frames.length; i += 1) {
684
+ if (!(i in frames)) return null;
685
+ const f = frames[i];
686
+ if (typeof f !== "string" || f.length > REPORT_FRAME_MAX_LENGTH) return null;
687
+ out.push(f);
688
+ }
689
+ return out;
690
+ }
691
+ function validateReport(payload) {
692
+ const kind = TEMPLATE_REPORT_KINDS.find((k) => k === field(payload, "kind"));
693
+ const message = field(payload, "message");
694
+ const frames = framesCopy(field(payload, "frames"));
695
+ const count = field(payload, "count");
696
+ if (!kind || typeof message !== "string" || message.length > REPORT_MESSAGE_MAX_LENGTH || frames === null || typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > REPORT_COUNT_MAX) {
697
+ throw new BridgeError("BAD_REQUEST", "invalid report");
698
+ }
699
+ return Object.freeze({
700
+ kind,
701
+ message,
702
+ frames: Object.freeze(frames),
703
+ count
704
+ });
705
+ }
671
706
 
672
707
  // src/host/createBridgeHost.ts
673
708
  var OPAQUE_ORIGIN = "null";
@@ -805,6 +840,16 @@ async function dispatch(ctx, type, payload) {
805
840
  else writeConsole(level, message);
806
841
  return EMPTY;
807
842
  }
843
+ case "report": {
844
+ const report = validateReport(payload);
845
+ const onReport = ctx.options.onTemplateReport;
846
+ if (!onReport || !ctx.takeReportSlot()) return EMPTY;
847
+ try {
848
+ onReport(report);
849
+ } catch {
850
+ }
851
+ return EMPTY;
852
+ }
808
853
  }
809
854
  }
810
855
  function failure(code, message) {
@@ -848,6 +893,9 @@ function assertOptions(options) {
848
893
  if (options.onDispose !== void 0 && typeof options.onDispose !== "function") {
849
894
  throw new TypeError("[bstage-bridge] onDispose\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
850
895
  }
896
+ if (options.onTemplateReport !== void 0 && typeof options.onTemplateReport !== "function") {
897
+ throw new TypeError("[bstage-bridge] onTemplateReport\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
898
+ }
851
899
  if (!options.iframe || !options.init) {
852
900
  throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
853
901
  }
@@ -869,6 +917,7 @@ function createBridgeHost(options) {
869
917
  const resizeLimiter = createRateLimiter(RESIZE_PER_SECOND, 1e3);
870
918
  let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
871
919
  let inFlightFetches = 0;
920
+ let reportsTaken = 0;
872
921
  let disposed = false;
873
922
  let boundFrame;
874
923
  let pendingVisibility;
@@ -898,7 +947,12 @@ function createBridgeHost(options) {
898
947
  externalFetchImpl: options.externalFetchImpl ?? defaultUncredentialedFetch,
899
948
  hostOrigin,
900
949
  toastLimiter: createRateLimiter(TOAST_PER_MINUTE),
901
- scheduleResize
950
+ scheduleResize,
951
+ takeReportSlot() {
952
+ if (reportsTaken >= REPORTS_PER_SESSION) return false;
953
+ reportsTaken += 1;
954
+ return true;
955
+ }
902
956
  };
903
957
  function post(target, envelope) {
904
958
  if (disposed) return;
@@ -1338,6 +1392,232 @@ function createBridgeFetch(client) {
1338
1392
  return bridgeFetch;
1339
1393
  }
1340
1394
 
1395
+ // src/frame/directFetch.ts
1396
+ function installDirectFetch(g, client, config) {
1397
+ const hostOrigin = config.hostOrigin;
1398
+ const callFn = Function.prototype.call;
1399
+ const NativeURL = g.URL;
1400
+ const rawFetch = g.fetch;
1401
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
1402
+ const bridgeFetch = client.fetch;
1403
+ const strSlice = callFn.bind(String.prototype.slice);
1404
+ const promiseThen = callFn.bind(Promise.prototype.then);
1405
+ const promiseReject = Promise.reject.bind(Promise);
1406
+ const assign = Object.assign;
1407
+ const TypeErr = TypeError;
1408
+ function getter(ctor, name) {
1409
+ if (typeof ctor !== "function") return void 0;
1410
+ let proto = ctor.prototype;
1411
+ while (proto) {
1412
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
1413
+ if (desc && typeof desc.get === "function") {
1414
+ return callFn.bind(desc.get);
1415
+ }
1416
+ proto = Object.getPrototypeOf(proto);
1417
+ }
1418
+ return void 0;
1419
+ }
1420
+ const getHref = getter(NativeURL, "href");
1421
+ const getOrigin = getter(NativeURL, "origin");
1422
+ const getProtocol = getter(NativeURL, "protocol");
1423
+ const getPathname = getter(NativeURL, "pathname");
1424
+ const getRequestUrl = getter(g.Request, "url");
1425
+ let gatewayPrefixes = null;
1426
+ let partnerOrigins = [];
1427
+ let routesReady = false;
1428
+ function copyStrings(value) {
1429
+ if (!Array.isArray(value)) return null;
1430
+ const out = [];
1431
+ for (let i = 0; i < value.length; i += 1) {
1432
+ if (typeof value[i] === "string") out[out.length] = value[i];
1433
+ }
1434
+ return out;
1435
+ }
1436
+ function takeSnapshot(init) {
1437
+ if (routesReady) return;
1438
+ const raw = init;
1439
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
1440
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
1441
+ routesReady = true;
1442
+ }
1443
+ promiseThen(client.ready, takeSnapshot);
1444
+ function matchesPrefix2(pathname, prefix) {
1445
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
1446
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
1447
+ }
1448
+ function routeOf(origin, protocol, pathname) {
1449
+ if (origin === hostOrigin) {
1450
+ if (gatewayPrefixes === null) return "bridge";
1451
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
1452
+ if (matchesPrefix2(pathname, gatewayPrefixes[i])) return "bridge";
1453
+ }
1454
+ return "reject";
1455
+ }
1456
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
1457
+ if (origin === partnerOrigins[i]) return "bridge";
1458
+ }
1459
+ return protocol === "https:" ? "direct" : "reject";
1460
+ }
1461
+ function requestUrlOf(input) {
1462
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
1463
+ try {
1464
+ return getRequestUrl(input);
1465
+ } catch {
1466
+ return void 0;
1467
+ }
1468
+ }
1469
+ function dispatch2(input, init) {
1470
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
1471
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
1472
+ }
1473
+ const requestUrl = requestUrlOf(input);
1474
+ const request = requestUrl !== void 0;
1475
+ let parsed;
1476
+ try {
1477
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
1478
+ } catch {
1479
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
1480
+ }
1481
+ const href = getHref(parsed);
1482
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
1483
+ const target = request ? input : href;
1484
+ if (route === "bridge") return bridgeFetch(target, init);
1485
+ if (route === "reject") {
1486
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
1487
+ }
1488
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
1489
+ return nativeFetch(
1490
+ g,
1491
+ target,
1492
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
1493
+ );
1494
+ }
1495
+ function bstageFetch(input, init) {
1496
+ try {
1497
+ if (routesReady) return dispatch2(input, init);
1498
+ return promiseThen(client.ready, function(value) {
1499
+ takeSnapshot(value);
1500
+ return dispatch2(input, init);
1501
+ });
1502
+ } catch {
1503
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
1504
+ }
1505
+ }
1506
+ Object.defineProperty(g, "__bstage_fetch__", {
1507
+ value: bstageFetch,
1508
+ writable: false,
1509
+ configurable: false,
1510
+ enumerable: false
1511
+ });
1512
+ return bstageFetch;
1513
+ }
1514
+
1515
+ // src/frame/errorReport.ts
1516
+ function installErrorReport(g, client, config) {
1517
+ const schedule = typeof g.setTimeout === "function" ? g.setTimeout : void 0;
1518
+ const entries = /* @__PURE__ */ new Map();
1519
+ let admitted = 0;
1520
+ function toText(value) {
1521
+ if (typeof value === "string") return value;
1522
+ try {
1523
+ return String(value);
1524
+ } catch {
1525
+ return "";
1526
+ }
1527
+ }
1528
+ function framesOf(stack) {
1529
+ const out = [];
1530
+ if (typeof stack !== "string") return out;
1531
+ const lines = stack.split("\n");
1532
+ for (let i = 0; i < lines.length && out.length < config.maxFrames; i += 1) {
1533
+ const line = lines[i].trim();
1534
+ if (/^at\s/.test(line) || /^[^@]*@(?:[a-z][\w+.-]*:\/\/|\/)\S*:\d+:\d+$/i.test(line) || /^(?:[a-z][\w+.-]*:\/\/|\/)\S*:\d+:\d+$/i.test(line))
1535
+ out.push(line.slice(0, config.maxFrameLength));
1536
+ }
1537
+ return out;
1538
+ }
1539
+ function send(key) {
1540
+ const entry = entries.get(key);
1541
+ if (!entry || entry.sent) return;
1542
+ entries.set(key, {
1543
+ kind: entry.kind,
1544
+ message: entry.message,
1545
+ frames: entry.frames,
1546
+ count: entry.count,
1547
+ sent: true
1548
+ });
1549
+ try {
1550
+ client.request("report", {
1551
+ kind: entry.kind,
1552
+ message: entry.message,
1553
+ frames: entry.frames,
1554
+ count: entry.count
1555
+ }).catch(function() {
1556
+ });
1557
+ } catch {
1558
+ }
1559
+ }
1560
+ function record(kind, message, stack) {
1561
+ try {
1562
+ const text = toText(message).slice(0, config.maxMessageLength);
1563
+ const frames = framesOf(stack);
1564
+ const key = kind + "|" + text.replace(/\d+/g, "") + "|" + (frames[0] || "");
1565
+ const entry = entries.get(key);
1566
+ if (entry) {
1567
+ if (!entry.sent && entry.count < config.maxCount) {
1568
+ entries.set(key, {
1569
+ kind: entry.kind,
1570
+ message: entry.message,
1571
+ frames: entry.frames,
1572
+ count: entry.count + 1,
1573
+ sent: false
1574
+ });
1575
+ }
1576
+ return;
1577
+ }
1578
+ if (admitted >= config.maxReports) return;
1579
+ admitted += 1;
1580
+ entries.set(key, { kind, message: text, frames, count: 1, sent: false });
1581
+ if (admitted >= config.maxReports) stop();
1582
+ if (schedule) {
1583
+ schedule(function() {
1584
+ send(key);
1585
+ }, config.flushDelayMs);
1586
+ } else {
1587
+ send(key);
1588
+ }
1589
+ } catch {
1590
+ }
1591
+ }
1592
+ function onError(ev) {
1593
+ const err = ev && ev.error;
1594
+ if (err && typeof err === "object") {
1595
+ record("error", typeof err.message === "string" ? err.message : ev.message, err.stack);
1596
+ return;
1597
+ }
1598
+ const where = ev && typeof ev.filename === "string" && ev.filename !== "" ? "at " + ev.filename + ":" + toText(ev.lineno) + ":" + toText(ev.colno) : "";
1599
+ record("error", ev ? ev.message : "", where);
1600
+ }
1601
+ function onRejection(ev) {
1602
+ const reason = ev ? ev.reason : void 0;
1603
+ if (reason && typeof reason === "object" && typeof reason.message === "string") {
1604
+ record("rejection", reason.message, reason.stack);
1605
+ return;
1606
+ }
1607
+ record("rejection", reason, "");
1608
+ }
1609
+ function stop() {
1610
+ if (typeof g.removeEventListener !== "function") return;
1611
+ g.removeEventListener("error", onError);
1612
+ g.removeEventListener("unhandledrejection", onRejection);
1613
+ }
1614
+ if (typeof g.addEventListener === "function") {
1615
+ g.addEventListener("error", onError);
1616
+ g.addEventListener("unhandledrejection", onRejection);
1617
+ }
1618
+ return record;
1619
+ }
1620
+
1341
1621
  // src/srcdoc/escape.ts
1342
1622
  var HTML_ESCAPES = {
1343
1623
  "&": "&amp;",
@@ -1355,6 +1635,7 @@ function toScriptLiteral(value) {
1355
1635
 
1356
1636
  // src/srcdoc/createSrcdoc.ts
1357
1637
  var MAX_MEDIA_ORIGINS = 20;
1638
+ var REPORT_FLUSH_DELAY_MS = 1e3;
1358
1639
  var MAX_ELEMENT_NAME_LENGTH = 100;
1359
1640
  var ELEMENT_NAME_RE = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
1360
1641
  function isValidElementName(value) {
@@ -1403,7 +1684,7 @@ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
1403
1684
  `img-src ${media} data: blob:`,
1404
1685
  `font-src ${assetOrigin} data:`,
1405
1686
  `media-src ${media} blob:`,
1406
- "connect-src 'none'",
1687
+ "connect-src https: wss:",
1407
1688
  "frame-src 'none'",
1408
1689
  "form-action 'none'",
1409
1690
  "base-uri 'none'"
@@ -1420,6 +1701,19 @@ function bootstrapScript(o) {
1420
1701
  `var bridgeRuntime = ${bridgeRuntime.toString()};`,
1421
1702
  `var createBridgeFetch = ${createBridgeFetch.toString()};`,
1422
1703
  "var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
1704
+ // 라우터는 항상 심는다. ready.then이 엔트리 로드(아래 client.ready.then)보다 먼저 등록돼야 템플릿보다 먼저 스냅샷을 잡는다.
1705
+ `var installDirectFetch = ${installDirectFetch.toString()};`,
1706
+ "installDirectFetch(globalThis, client, { hostOrigin: HOST_ORIGIN });",
1707
+ // 에러 수집기는 엔트리(템플릿)보다 먼저 리스너를 건다. 상한은 호스트 검증기와 같은 프로토콜 값이다.
1708
+ `var installErrorReport = ${installErrorReport.toString()};`,
1709
+ "var reportError = installErrorReport(globalThis, client, {",
1710
+ ` maxReports: ${REPORTS_PER_SESSION},`,
1711
+ ` maxMessageLength: ${REPORT_MESSAGE_MAX_LENGTH},`,
1712
+ ` maxFrames: ${REPORT_MAX_FRAMES},`,
1713
+ ` maxFrameLength: ${REPORT_FRAME_MAX_LENGTH},`,
1714
+ ` maxCount: ${REPORT_COUNT_MAX},`,
1715
+ ` flushDelayMs: ${REPORT_FLUSH_DELAY_MS}`,
1716
+ "});",
1423
1717
  "var lastHeight = -1;",
1424
1718
  "var scheduled = false;",
1425
1719
  "function reportHeight() {",
@@ -1436,9 +1730,12 @@ function bootstrapScript(o) {
1436
1730
  "}",
1437
1731
  "client.ready.then(function () {",
1438
1732
  " var script = document.createElement('script');",
1733
+ // crossOrigin은 src보다 먼저 둔다(요청 모드가 src 대입 시점에 정해지는 브라우저가 있다). 주지 않으면 줄 자체가 없다.
1734
+ ...o.crossOrigin === "anonymous" ? [" script.crossOrigin = 'anonymous';"] : [],
1439
1735
  " script.src = ENTRY_URL;",
1440
1736
  " script.onerror = function () {",
1441
1737
  " client.request('log', { level: 'error', message: 'template entry failed to load' }).catch(function () {});",
1738
+ " reportError('load', 'template entry failed to load', '');",
1442
1739
  " };",
1443
1740
  " document.head.appendChild(script);",
1444
1741
  " customElements.whenDefined(ELEMENT_NAME).then(function () {",
@@ -1464,8 +1761,12 @@ function createSrcdoc(options) {
1464
1761
  }
1465
1762
  if (typeof title !== "string") throw new TypeError("[bstage-bridge] title\uC740 \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
1466
1763
  const mediaOrigins = normalizeMediaOrigins(options.mediaOrigins);
1764
+ const { crossOrigin } = options;
1765
+ if (crossOrigin !== void 0 && crossOrigin !== "anonymous") {
1766
+ throw new TypeError("[bstage-bridge] crossOrigin\uC740 'anonymous'\uB9CC \uBC1B\uC2B5\uB2C8\uB2E4");
1767
+ }
1467
1768
  const csp = buildFrameCsp(nonce, assetOrigin, mediaOrigins);
1468
- const script = bootstrapScript({ session, hostOrigin, entryUrl, elementName });
1769
+ const script = bootstrapScript({ session, hostOrigin, entryUrl, elementName, crossOrigin });
1469
1770
  if (/<\/script|<!--/i.test(script)) {
1470
1771
  throw new Error("[bstage-bridge] \uBD80\uD2B8\uC2A4\uD2B8\uB7A9 \uC2A4\uD06C\uB9BD\uD2B8\uC5D0 \uC2A4\uD06C\uB9BD\uD2B8 \uC885\uB8CC \uC2DC\uD000\uC2A4\uAC00 \uC788\uC2B5\uB2C8\uB2E4");
1471
1772
  }
@@ -1499,6 +1800,7 @@ ${script}
1499
1800
  IFRAME_ATTRIBUTES,
1500
1801
  MAX_DECODE_PASSES,
1501
1802
  MAX_PARTNER_ORIGINS,
1803
+ REPORTS_PER_SESSION,
1502
1804
  checkPath,
1503
1805
  classifyDestination,
1504
1806
  createBridgeHost,
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, C as ToastKind, L as LogLevel, l as BridgeSurface, A as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations, y as TemplateReport } from './messages-Cdjpvu2S.cjs';
2
+ export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, q as REPORTS_PER_SESSION, z as TemplateReportKind, G as isBridgeSurface } from './messages-Cdjpvu2S.cjs';
3
+ export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-DYZk6dDJ.cjs';
4
4
 
5
5
  /** 호스트 fetch 정책. */
6
6
  interface BridgePolicy {
@@ -219,6 +219,14 @@ interface CreateBridgeHostOptions {
219
219
  * 바로 걷어 내는 데 쓴다. 이 콜백의 예외는 삼킨다.
220
220
  */
221
221
  readonly onDispose?: (reason: BridgeDisposeReason) => void;
222
+ /**
223
+ * 프레임의 `report`(템플릿 에러 보고)를 검증한 뒤 부른다. phase와 무관하게(real 포함) 불린다.
224
+ * 프레임 세션(이 호스트)당 `REPORTS_PER_SESSION`(5)건까지만 부르고 넘치는 보고는 조용히 버린다.
225
+ * 넘기는 값은 `{ kind, message, frames, count }`뿐이다 — 어느 템플릿인지는 프레임이 보낸 값이 아니라
226
+ * 호스트가 띄운 templateUrl로 임베더가 귀속한다. message·frames는 템플릿이 만든 문자열이라 텍스트로만 다루고,
227
+ * 밖으로 보내기 전 스크러빙은 임베더 몫이다. 이 콜백의 예외는 삼킨다.
228
+ */
229
+ readonly onTemplateReport?: (report: TemplateReport) => void;
222
230
  }
223
231
  /** `onDispose` 사유. */
224
232
  type BridgeDisposeReason = 'frame-unload' | 'frame-mismatch' | 'manual';
@@ -277,4 +285,4 @@ interface PathCheck {
277
285
  */
278
286
  declare function checkPath(pathname: string, options?: PathCheckOptions): PathCheck;
279
287
 
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 };
288
+ 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, TemplateReport, 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, 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, C as ToastKind, L as LogLevel, l as BridgeSurface, A as ThemeState, S as SlotState, M as MemberState, m as BridgeTranslations, y as TemplateReport } from './messages-Cdjpvu2S.js';
2
+ export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES, q as REPORTS_PER_SESSION, z as TemplateReportKind, G as isBridgeSurface } from './messages-Cdjpvu2S.js';
3
+ export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-DYZk6dDJ.js';
4
4
 
5
5
  /** 호스트 fetch 정책. */
6
6
  interface BridgePolicy {
@@ -219,6 +219,14 @@ interface CreateBridgeHostOptions {
219
219
  * 바로 걷어 내는 데 쓴다. 이 콜백의 예외는 삼킨다.
220
220
  */
221
221
  readonly onDispose?: (reason: BridgeDisposeReason) => void;
222
+ /**
223
+ * 프레임의 `report`(템플릿 에러 보고)를 검증한 뒤 부른다. phase와 무관하게(real 포함) 불린다.
224
+ * 프레임 세션(이 호스트)당 `REPORTS_PER_SESSION`(5)건까지만 부르고 넘치는 보고는 조용히 버린다.
225
+ * 넘기는 값은 `{ kind, message, frames, count }`뿐이다 — 어느 템플릿인지는 프레임이 보낸 값이 아니라
226
+ * 호스트가 띄운 templateUrl로 임베더가 귀속한다. message·frames는 템플릿이 만든 문자열이라 텍스트로만 다루고,
227
+ * 밖으로 보내기 전 스크러빙은 임베더 몫이다. 이 콜백의 예외는 삼킨다.
228
+ */
229
+ readonly onTemplateReport?: (report: TemplateReport) => void;
222
230
  }
223
231
  /** `onDispose` 사유. */
224
232
  type BridgeDisposeReason = 'frame-unload' | 'frame-mismatch' | 'manual';
@@ -277,4 +285,4 @@ interface PathCheck {
277
285
  */
278
286
  declare function checkPath(pathname: string, options?: PathCheckOptions): PathCheck;
279
287
 
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 };
288
+ 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, TemplateReport, checkPath, classifyDestination, createBridgeHost, isAllowedGatewayPath, normalizePartnerOrigins, normalizePinnedHeaders, prepareFetch, resolveDestination, resolvePolicy };
package/dist/host.js CHANGED
@@ -12,6 +12,12 @@ import {
12
12
  MAX_DECODE_PASSES,
13
13
  MAX_PARTNER_ORIGINS,
14
14
  READY_EVENT,
15
+ REPORTS_PER_SESSION,
16
+ REPORT_COUNT_MAX,
17
+ REPORT_FRAME_MAX_LENGTH,
18
+ REPORT_MAX_FRAMES,
19
+ REPORT_MESSAGE_MAX_LENGTH,
20
+ TEMPLATE_REPORT_KINDS,
15
21
  checkPath,
16
22
  classifyDestination,
17
23
  createEnvelope,
@@ -32,8 +38,8 @@ import {
32
38
  prepareFetch,
33
39
  resolveDestination,
34
40
  resolvePolicy
35
- } from "./chunk-JH3A3Z2A.js";
36
- import "./chunk-N3JID7KQ.js";
41
+ } from "./chunk-NXB47KPE.js";
42
+ import "./chunk-TPKNGW34.js";
37
43
 
38
44
  // src/host/rateLimiter.ts
39
45
  var DEFAULT_WINDOW_MS = 6e4;
@@ -115,6 +121,7 @@ function buildInit(init, hostOrigin, partnerOrigins, policy) {
115
121
  spaceId: String(init.spaceId),
116
122
  hostOrigin,
117
123
  gatewayBase: `${hostOrigin}/gw`,
124
+ gatewayPrefixes: policy.gatewayPrefixes,
118
125
  locale: String(init.locale),
119
126
  theme: sanitizeTheme(init.theme),
120
127
  ...slot ? { slot } : {},
@@ -277,6 +284,32 @@ function validateLog(payload) {
277
284
  if (!known || typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid log");
278
285
  return { level: known, message: message.slice(0, LOG_MAX_LENGTH) };
279
286
  }
287
+ function framesCopy(frames) {
288
+ if (!Array.isArray(frames) || frames.length > REPORT_MAX_FRAMES) return null;
289
+ const out = [];
290
+ for (let i = 0; i < frames.length; i += 1) {
291
+ if (!(i in frames)) return null;
292
+ const f = frames[i];
293
+ if (typeof f !== "string" || f.length > REPORT_FRAME_MAX_LENGTH) return null;
294
+ out.push(f);
295
+ }
296
+ return out;
297
+ }
298
+ function validateReport(payload) {
299
+ const kind = TEMPLATE_REPORT_KINDS.find((k) => k === field(payload, "kind"));
300
+ const message = field(payload, "message");
301
+ const frames = framesCopy(field(payload, "frames"));
302
+ const count = field(payload, "count");
303
+ if (!kind || typeof message !== "string" || message.length > REPORT_MESSAGE_MAX_LENGTH || frames === null || typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > REPORT_COUNT_MAX) {
304
+ throw new BridgeError("BAD_REQUEST", "invalid report");
305
+ }
306
+ return Object.freeze({
307
+ kind,
308
+ message,
309
+ frames: Object.freeze(frames),
310
+ count
311
+ });
312
+ }
280
313
 
281
314
  // src/host/createBridgeHost.ts
282
315
  var OPAQUE_ORIGIN = "null";
@@ -414,6 +447,16 @@ async function dispatch(ctx, type, payload) {
414
447
  else writeConsole(level, message);
415
448
  return EMPTY;
416
449
  }
450
+ case "report": {
451
+ const report = validateReport(payload);
452
+ const onReport = ctx.options.onTemplateReport;
453
+ if (!onReport || !ctx.takeReportSlot()) return EMPTY;
454
+ try {
455
+ onReport(report);
456
+ } catch {
457
+ }
458
+ return EMPTY;
459
+ }
417
460
  }
418
461
  }
419
462
  function failure(code, message) {
@@ -457,6 +500,9 @@ function assertOptions(options) {
457
500
  if (options.onDispose !== void 0 && typeof options.onDispose !== "function") {
458
501
  throw new TypeError("[bstage-bridge] onDispose\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
459
502
  }
503
+ if (options.onTemplateReport !== void 0 && typeof options.onTemplateReport !== "function") {
504
+ throw new TypeError("[bstage-bridge] onTemplateReport\uB294 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
505
+ }
460
506
  if (!options.iframe || !options.init) {
461
507
  throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
462
508
  }
@@ -478,6 +524,7 @@ function createBridgeHost(options) {
478
524
  const resizeLimiter = createRateLimiter(RESIZE_PER_SECOND, 1e3);
479
525
  let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
480
526
  let inFlightFetches = 0;
527
+ let reportsTaken = 0;
481
528
  let disposed = false;
482
529
  let boundFrame;
483
530
  let pendingVisibility;
@@ -507,7 +554,12 @@ function createBridgeHost(options) {
507
554
  externalFetchImpl: options.externalFetchImpl ?? defaultUncredentialedFetch,
508
555
  hostOrigin,
509
556
  toastLimiter: createRateLimiter(TOAST_PER_MINUTE),
510
- scheduleResize
557
+ scheduleResize,
558
+ takeReportSlot() {
559
+ if (reportsTaken >= REPORTS_PER_SESSION) return false;
560
+ reportsTaken += 1;
561
+ return true;
562
+ }
511
563
  };
512
564
  function post(target, envelope) {
513
565
  if (disposed) return;
@@ -614,6 +666,7 @@ export {
614
666
  IFRAME_ATTRIBUTES,
615
667
  MAX_DECODE_PASSES,
616
668
  MAX_PARTNER_ORIGINS,
669
+ REPORTS_PER_SESSION,
617
670
  checkPath,
618
671
  classifyDestination,
619
672
  createBridgeHost,