@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/README.md +77 -4
- package/dist/{chunk-JH3A3Z2A.js → chunk-NXB47KPE.js} +42 -5
- package/dist/{chunk-N3JID7KQ.js → chunk-TPKNGW34.js} +229 -1
- package/dist/{createSrcdoc-bOoxttPz.d.cts → createSrcdoc-DYZk6dDJ.d.cts} +16 -2
- package/dist/{createSrcdoc-bOoxttPz.d.ts → createSrcdoc-DYZk6dDJ.d.ts} +16 -2
- package/dist/frame.cjs +232 -2
- package/dist/frame.d.cts +87 -2
- package/dist/frame.d.ts +87 -2
- package/dist/frame.js +7 -3
- package/dist/host.cjs +306 -4
- package/dist/host.d.cts +12 -4
- package/dist/host.d.ts +12 -4
- package/dist/host.js +56 -3
- package/dist/index.cjs +271 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -2
- package/dist/{messages-CRGnyf-6.d.cts → messages-Cdjpvu2S.d.cts} +35 -2
- package/dist/{messages-CRGnyf-6.d.ts → messages-Cdjpvu2S.d.ts} +35 -2
- package/package.json +1 -1
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
|
"&": "&",
|
|
@@ -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
|
|
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,
|
|
2
|
-
export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES,
|
|
3
|
-
export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-
|
|
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,
|
|
2
|
-
export { a as BRIDGE_SURFACES, I as IFRAME_ATTRIBUTES,
|
|
3
|
-
export { C as CreateSrcdocOptions, c as createNonce, d as createSession, e as createSrcdoc, f as isValidSession } from './createSrcdoc-
|
|
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-
|
|
36
|
-
import "./chunk-
|
|
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,
|