@bstage-sdk/bridge 0.0.0-stage → 0.9.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 ADDED
@@ -0,0 +1,1399 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/host/index.ts
21
+ var host_exports = {};
22
+ __export(host_exports, {
23
+ ALLOWED_METHODS: () => ALLOWED_METHODS,
24
+ ALLOWED_REQUEST_HEADERS: () => ALLOWED_REQUEST_HEADERS,
25
+ BRIDGE_SURFACES: () => BRIDGE_SURFACES,
26
+ DEFAULT_GATEWAY_DENY_SEGMENTS: () => DEFAULT_GATEWAY_DENY_SEGMENTS,
27
+ DEFAULT_POLICY: () => DEFAULT_POLICY,
28
+ HOST_ONLY_HEADERS: () => HOST_ONLY_HEADERS,
29
+ IFRAME_ATTRIBUTES: () => IFRAME_ATTRIBUTES,
30
+ MAX_DECODE_PASSES: () => MAX_DECODE_PASSES,
31
+ MAX_PARTNER_ORIGINS: () => MAX_PARTNER_ORIGINS,
32
+ checkPath: () => checkPath,
33
+ classifyDestination: () => classifyDestination,
34
+ createBridgeHost: () => createBridgeHost,
35
+ createNonce: () => createNonce,
36
+ createSession: () => createSession,
37
+ createSrcdoc: () => createSrcdoc,
38
+ isAllowedGatewayPath: () => isAllowedGatewayPath,
39
+ isBridgeSurface: () => isBridgeSurface,
40
+ isValidSession: () => isValidSession,
41
+ normalizePartnerOrigins: () => normalizePartnerOrigins,
42
+ normalizePinnedHeaders: () => normalizePinnedHeaders,
43
+ prepareFetch: () => prepareFetch,
44
+ resolveDestination: () => resolveDestination,
45
+ resolvePolicy: () => resolvePolicy
46
+ });
47
+ module.exports = __toCommonJS(host_exports);
48
+
49
+ // src/protocol/envelope.ts
50
+ var BRIDGE_VERSION = 1;
51
+ var BRIDGE_NAMESPACE = "bstage-bridge";
52
+ var MAX_ENVELOPE_ID_LENGTH = 64;
53
+ var MAX_ENVELOPE_TYPE_LENGTH = 32;
54
+ var ENVELOPE_KINDS = ["req", "res", "evt"];
55
+ function isNonEmptyString(value, max) {
56
+ return typeof value === "string" && value.length > 0 && value.length <= max;
57
+ }
58
+ function isBridgeEnvelope(value) {
59
+ if (typeof value !== "object" || value === null) return false;
60
+ const env = value;
61
+ return env.v === BRIDGE_VERSION && env.ns === BRIDGE_NAMESPACE && isNonEmptyString(env.id, MAX_ENVELOPE_ID_LENGTH) && ENVELOPE_KINDS.includes(env.kind) && isNonEmptyString(env.type, MAX_ENVELOPE_TYPE_LENGTH) && "payload" in env;
62
+ }
63
+ function createEnvelope(kind, type, id, payload) {
64
+ return { v: BRIDGE_VERSION, ns: BRIDGE_NAMESPACE, id, kind, type, payload };
65
+ }
66
+ function hasSession(payload, session) {
67
+ return typeof payload === "object" && payload !== null && payload.session === session;
68
+ }
69
+
70
+ // src/protocol/errors.ts
71
+ var BRIDGE_ERROR_CODES = [
72
+ "FORBIDDEN_DESTINATION",
73
+ "FORBIDDEN_METHOD",
74
+ "BODY_TOO_LARGE",
75
+ "RATE_LIMITED",
76
+ "BAD_REQUEST",
77
+ "TIMEOUT",
78
+ "NETWORK_ERROR",
79
+ "UNSUPPORTED"
80
+ ];
81
+ var BridgeError = class extends Error {
82
+ constructor(code, message) {
83
+ super(message);
84
+ this.name = "BridgeError";
85
+ this.code = code;
86
+ }
87
+ };
88
+ function isBridgeErrorCode(value) {
89
+ return BRIDGE_ERROR_CODES.includes(value);
90
+ }
91
+ function isBridgeErrorLike(value) {
92
+ return value instanceof Error && isBridgeErrorCode(value.code);
93
+ }
94
+
95
+ // src/protocol/messages.ts
96
+ var REQUEST_TYPES = [
97
+ "fetch",
98
+ "navigate",
99
+ "back",
100
+ "openExternal",
101
+ "toast",
102
+ "resize",
103
+ "track",
104
+ "storage",
105
+ "log"
106
+ ];
107
+ var HOST_EVENT_TYPES = ["slot.context", "locale", "member", "theme", "visibility"];
108
+ var READY_EVENT = "ready";
109
+ var INIT_RESPONSE = "init";
110
+ var BRIDGE_SURFACES = ["page", "slot"];
111
+ function isBridgeSurface(value) {
112
+ return BRIDGE_SURFACES.includes(value);
113
+ }
114
+ var IFRAME_ATTRIBUTES = {
115
+ sandbox: "allow-scripts",
116
+ allow: "autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write",
117
+ referrerpolicy: "no-referrer"
118
+ };
119
+ function isRequestType(value) {
120
+ return REQUEST_TYPES.includes(value);
121
+ }
122
+ function isHostEventType(value) {
123
+ return HOST_EVENT_TYPES.includes(value);
124
+ }
125
+
126
+ // src/protocol/session.ts
127
+ var SESSION_RE = /^[0-9a-f]{32}$/;
128
+ var NONCE_RE = /^[A-Za-z0-9+/_-]{22,}={0,2}$/;
129
+ function randomBytes(length) {
130
+ const bytes = new Uint8Array(length);
131
+ globalThis.crypto.getRandomValues(bytes);
132
+ return bytes;
133
+ }
134
+ function createSession() {
135
+ return Array.from(randomBytes(16), (b) => b.toString(16).padStart(2, "0")).join("");
136
+ }
137
+ function isValidSession(value) {
138
+ return typeof value === "string" && SESSION_RE.test(value);
139
+ }
140
+ function createNonce() {
141
+ const binary = String.fromCharCode(...randomBytes(16));
142
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
143
+ }
144
+ function isValidNonce(value) {
145
+ return typeof value === "string" && NONCE_RE.test(value);
146
+ }
147
+
148
+ // src/host/pathSafety.ts
149
+ var MAX_DECODE_PASSES = 3;
150
+ var REJECT = Object.freeze({ ok: false, forms: Object.freeze([]) });
151
+ function decodeStages(pathname) {
152
+ const forms = [pathname];
153
+ let current = pathname;
154
+ for (let pass = 0; pass <= MAX_DECODE_PASSES; pass += 1) {
155
+ let next;
156
+ try {
157
+ next = decodeURIComponent(current);
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ if (next === current) return forms;
162
+ if (pass === MAX_DECODE_PASSES) return void 0;
163
+ forms.push(next);
164
+ current = next;
165
+ }
166
+ return void 0;
167
+ }
168
+ var UNSAFE_CHARS_RE = /[\\;\u0000-\u001f\u007f]/;
169
+ function hasDotSegment(form) {
170
+ return form.split("/").some((segment) => segment === "." || segment === "..");
171
+ }
172
+ function checkPath(pathname, options = {}) {
173
+ const forms = decodeStages(pathname);
174
+ if (!forms) return REJECT;
175
+ const bad = forms.some(
176
+ (form) => UNSAFE_CHARS_RE.test(form) || hasDotSegment(form) || options.forbidDoubleSlash === true && form.includes("//")
177
+ );
178
+ return bad ? REJECT : { ok: true, forms: Object.freeze(forms) };
179
+ }
180
+
181
+ // src/host/policy.ts
182
+ var DEFAULT_GATEWAY_DENY_SEGMENTS = Object.freeze([
183
+ "auth",
184
+ "logout",
185
+ "mfa",
186
+ "token",
187
+ "oauth",
188
+ "sso",
189
+ "debug",
190
+ "debugger",
191
+ "xdebug",
192
+ "withdraw"
193
+ ]);
194
+ var ALWAYS_DENIED_SEGMENT_SUBSTRINGS = Object.freeze(["token"]);
195
+ var DEFAULT_POLICY = Object.freeze({
196
+ gatewayPrefixes: Object.freeze(["/gw/"]),
197
+ gatewayDenyPrefixes: Object.freeze(["/gw/account/v1/auth/", "/gw/account/v1/users/logout"]),
198
+ gatewayDenySegments: DEFAULT_GATEWAY_DENY_SEGMENTS,
199
+ allowExternal: false,
200
+ maxBodyBytes: 1024 * 1024,
201
+ maxRequestsPerMinute: 300,
202
+ maxResponseBytes: 10 * 1024 * 1024
203
+ });
204
+ var ALLOWED_REQUEST_HEADERS = Object.freeze([
205
+ "content-type",
206
+ "accept",
207
+ "accept-language",
208
+ // BstageClient가 보내는 앱 자격 헤더(파트너 콘솔 발급값, 브라우저 노출 전제 — 회원 토큰이 아니다).
209
+ // 호스트가 `pinnedHeaders`로 값을 주면 그 값이 이긴다.
210
+ "x-bstage-app-id",
211
+ "x-bstage-app-key",
212
+ // BstageClient가 모든 요청에 싣는 core 버전(정보용, 비밀 아님).
213
+ "x-bmf-sdk-version"
214
+ ]);
215
+ var HOST_ONLY_HEADERS = Object.freeze([
216
+ "x-bstage-tenant-id",
217
+ "x-bstage-org-id",
218
+ "x-bstage-enterprise-id"
219
+ ]);
220
+ var ALLOWED_METHODS = Object.freeze([
221
+ "GET",
222
+ "HEAD",
223
+ "POST",
224
+ "PUT",
225
+ "PATCH",
226
+ "DELETE"
227
+ ]);
228
+ var MAX_PARTNER_ORIGINS = 10;
229
+ var MAX_URL_LENGTH = 8192;
230
+ function isPositiveInteger(value) {
231
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
232
+ }
233
+ function resolvePolicy(policy) {
234
+ const merged = { ...DEFAULT_POLICY, ...policy ?? {} };
235
+ if (!Array.isArray(merged.gatewayPrefixes) || merged.gatewayPrefixes.length === 0 || !merged.gatewayPrefixes.every((p) => typeof p === "string" && /^\/[^/]/.test(p))) {
236
+ throw new TypeError('[bstage-bridge] gatewayPrefixes\uB294 "/"\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
237
+ }
238
+ if (!Array.isArray(merged.gatewayDenyPrefixes) || !merged.gatewayDenyPrefixes.every((p) => typeof p === "string" && /^\/[^/]/.test(p))) {
239
+ throw new TypeError(
240
+ '[bstage-bridge] gatewayDenyPrefixes\uB294 "/"\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4'
241
+ );
242
+ }
243
+ if (!Array.isArray(merged.gatewayDenySegments) || !merged.gatewayDenySegments.every(
244
+ (x) => typeof x === "string" && x.length > 0 && !x.includes("/")
245
+ )) {
246
+ throw new TypeError('[bstage-bridge] gatewayDenySegments\uB294 "/" \uC5C6\uB294 \uC138\uADF8\uBA3C\uD2B8 \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
247
+ }
248
+ if (typeof merged.allowExternal !== "boolean") {
249
+ throw new TypeError("[bstage-bridge] allowExternal\uC740 \uBD88\uB9AC\uC5B8\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
250
+ }
251
+ if (!isPositiveInteger(merged.maxBodyBytes) || !isPositiveInteger(merged.maxRequestsPerMinute) || !isPositiveInteger(merged.maxResponseBytes)) {
252
+ throw new TypeError(
253
+ "[bstage-bridge] maxBodyBytes\xB7maxRequestsPerMinute\xB7maxResponseBytes\uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4"
254
+ );
255
+ }
256
+ return Object.freeze({
257
+ ...merged,
258
+ gatewayPrefixes: Object.freeze([...merged.gatewayPrefixes]),
259
+ gatewayDenyPrefixes: Object.freeze([...merged.gatewayDenyPrefixes]),
260
+ gatewayDenySegments: Object.freeze(merged.gatewayDenySegments.map((x) => x.toLowerCase()))
261
+ });
262
+ }
263
+ var HOSTNAME_RE = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/;
264
+ function normalizeExactOrigin(value, protocols) {
265
+ if (typeof value !== "string") return void 0;
266
+ try {
267
+ const url = new URL(value);
268
+ if (!protocols.includes(url.protocol) || !HOSTNAME_RE.test(url.hostname)) return void 0;
269
+ return url.origin === value ? value : void 0;
270
+ } catch {
271
+ return void 0;
272
+ }
273
+ }
274
+ function normalizePartnerOrigins(origins, excludeOrigin) {
275
+ if (!Array.isArray(origins)) return Object.freeze([]);
276
+ const valid = origins.map((o) => normalizeExactOrigin(o, ["https:"])).filter((o) => o !== void 0 && o !== excludeOrigin);
277
+ return Object.freeze([...new Set(valid)].slice(0, MAX_PARTNER_ORIGINS));
278
+ }
279
+ function matchesPrefix(pathname, prefix) {
280
+ const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
281
+ return pathname === bare || pathname.startsWith(`${bare}/`);
282
+ }
283
+ function denyKey(form) {
284
+ return form.replace(/\/{2,}/g, "/").toLowerCase();
285
+ }
286
+ function isAllowedGatewayPath(pathname, policy) {
287
+ const check = checkPath(pathname);
288
+ if (!check.ok) return false;
289
+ if (!policy.gatewayPrefixes.some((p) => matchesPrefix(pathname, p))) return false;
290
+ const deny = policy.gatewayDenyPrefixes.map((p) => p.toLowerCase());
291
+ const denySegments = policy.gatewayDenySegments;
292
+ return !check.forms.some((form) => {
293
+ const key = denyKey(form);
294
+ return deny.some((p) => matchesPrefix(key, p)) || key.split("/").some(
295
+ (segment) => denySegments.includes(segment) || ALWAYS_DENIED_SEGMENT_SUBSTRINGS.some((part) => segment.includes(part))
296
+ );
297
+ });
298
+ }
299
+ var LOCAL_HOST_NAMES = Object.freeze([
300
+ "localhost",
301
+ "local",
302
+ "internal",
303
+ "home.arpa",
304
+ "test",
305
+ "invalid",
306
+ "example",
307
+ "lan",
308
+ "corp",
309
+ "intranet",
310
+ "localdomain"
311
+ ]);
312
+ function isExternalHostAllowed(hostname, hostHostname) {
313
+ const name = hostname.replace(/\.+$/, "");
314
+ if (name === "" || name.startsWith("[") || /^[0-9.]+$/.test(name)) return false;
315
+ const labels = name.split(".");
316
+ if (labels.length < 2 || labels.some((label) => label === "")) return false;
317
+ if (LOCAL_HOST_NAMES.some((base) => name === base || name.endsWith(`.${base}`))) return false;
318
+ return name !== hostHostname.replace(/\.+$/, "");
319
+ }
320
+ function classifyDestination(rawUrl, ctx) {
321
+ if (typeof rawUrl !== "string" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {
322
+ throw new BridgeError("BAD_REQUEST", "invalid url");
323
+ }
324
+ let url;
325
+ try {
326
+ url = new URL(rawUrl, ctx.hostOrigin);
327
+ } catch {
328
+ throw new BridgeError("BAD_REQUEST", "invalid url");
329
+ }
330
+ if (url.username !== "" || url.password !== "") {
331
+ throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
332
+ }
333
+ const host = new URL(ctx.hostOrigin);
334
+ if (url.origin === host.origin) {
335
+ if (url.protocol === host.protocol && isAllowedGatewayPath(url.pathname, ctx.policy)) {
336
+ return { url: url.href, kind: "gateway" };
337
+ }
338
+ throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
339
+ }
340
+ if (url.protocol !== "https:") {
341
+ throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
342
+ }
343
+ if (ctx.partnerOrigins.includes(url.origin)) return { url: url.href, kind: "partner" };
344
+ if (ctx.policy.allowExternal && isExternalHostAllowed(url.hostname, host.hostname)) {
345
+ return { url: url.href, kind: "external" };
346
+ }
347
+ throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
348
+ }
349
+ function resolveDestination(rawUrl, ctx) {
350
+ return classifyDestination(rawUrl, ctx).url;
351
+ }
352
+ function resolveMethod(method) {
353
+ if (method === void 0) return "GET";
354
+ if (typeof method !== "string") throw new BridgeError("BAD_REQUEST", "invalid method");
355
+ const upper = method.toUpperCase();
356
+ if (!ALLOWED_METHODS.includes(upper)) {
357
+ throw new BridgeError("FORBIDDEN_METHOD", "method not allowed");
358
+ }
359
+ return upper;
360
+ }
361
+ var UNSAFE_HEADER_VALUE_RE = /[\r\n\0]/;
362
+ function normalizePinnedHeaders(pinned) {
363
+ if (pinned === void 0) return Object.freeze({});
364
+ if (typeof pinned !== "object" || pinned === null || Array.isArray(pinned)) {
365
+ throw new TypeError("[bstage-bridge] pinnedHeaders\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4");
366
+ }
367
+ const entries = Object.entries(pinned).map(([k, v]) => {
368
+ const key = k.toLowerCase();
369
+ if (!ALLOWED_REQUEST_HEADERS.includes(key) && !HOST_ONLY_HEADERS.includes(key)) {
370
+ throw new TypeError(`[bstage-bridge] pinnedHeaders\uC5D0 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD5E4\uB354: ${k}`);
371
+ }
372
+ if (typeof v !== "string" || UNSAFE_HEADER_VALUE_RE.test(v)) {
373
+ throw new TypeError(`[bstage-bridge] pinnedHeaders \uAC12\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4: ${k}`);
374
+ }
375
+ return [key, v];
376
+ });
377
+ return Object.freeze(Object.fromEntries(entries));
378
+ }
379
+ function mergeHeaders(frameHeaders, pinned) {
380
+ return Object.freeze({ ...filterHeaders(frameHeaders), ...pinned });
381
+ }
382
+ function filterHeaders(headers) {
383
+ if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return {};
384
+ const entries = Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]).filter(
385
+ (entry) => ALLOWED_REQUEST_HEADERS.includes(entry[0]) && typeof entry[1] === "string" && !/[\r\n\0]/.test(entry[1])
386
+ );
387
+ return Object.freeze(Object.fromEntries(entries));
388
+ }
389
+ function exceedsBytes(body, max) {
390
+ if (body.length > max) return true;
391
+ if (body.length * 3 <= max) return false;
392
+ return new TextEncoder().encode(body).length > max;
393
+ }
394
+ function resolveBody(body, method, maxBodyBytes) {
395
+ if (body === void 0 || body === null) return void 0;
396
+ if (typeof body !== "string") throw new BridgeError("BAD_REQUEST", "body must be a string");
397
+ if (method === "GET" || method === "HEAD") {
398
+ throw new BridgeError("BAD_REQUEST", "GET/HEAD cannot have a body");
399
+ }
400
+ if (exceedsBytes(body, maxBodyBytes)) throw new BridgeError("BODY_TOO_LARGE", "body too large");
401
+ return body;
402
+ }
403
+ function prepareFetch(payload, ctx) {
404
+ if (typeof payload !== "object" || payload === null) {
405
+ throw new BridgeError("BAD_REQUEST", "invalid payload");
406
+ }
407
+ const req = payload;
408
+ const method = resolveMethod(req.method);
409
+ const { url, kind } = classifyDestination(req.url, ctx);
410
+ const body = resolveBody(req.body, method, ctx.policy.maxBodyBytes);
411
+ const headers = kind === "gateway" ? mergeHeaders(req.headers, ctx.pinnedHeaders ?? {}) : filterHeaders(req.headers);
412
+ return {
413
+ url,
414
+ kind,
415
+ init: {
416
+ method,
417
+ headers,
418
+ ...body !== void 0 ? { body } : {},
419
+ credentials: kind === "gateway" ? "same-origin" : "omit",
420
+ ...kind === "gateway" ? {} : { referrerPolicy: "no-referrer" },
421
+ // 브리지 프로토콜 인자에 더한 방어선: redirect를 따라가면 판정한 URL과 실제 목적지가 달라진다
422
+ // (파트너 origin이 같은 origin의 /gw 밖 경로로 돌려보내면 쿠키가 실린 요청이 된다).
423
+ redirect: "error"
424
+ }
425
+ };
426
+ }
427
+
428
+ // src/host/rateLimiter.ts
429
+ var DEFAULT_WINDOW_MS = 6e4;
430
+ function createRateLimiter(max, windowMs = DEFAULT_WINDOW_MS) {
431
+ let stamps = [];
432
+ return {
433
+ take(now = Date.now()) {
434
+ const recent = stamps.filter((t) => now - t < windowMs);
435
+ if (recent.length >= max) {
436
+ stamps = recent;
437
+ return false;
438
+ }
439
+ stamps = [...recent, now];
440
+ return true;
441
+ }
442
+ };
443
+ }
444
+
445
+ // src/host/events.ts
446
+ function obj(value) {
447
+ return typeof value === "object" && value !== null ? value : {};
448
+ }
449
+ function sanitizeTheme(value) {
450
+ return { dark: obj(value).dark === true };
451
+ }
452
+ function sanitizeMember(value) {
453
+ const m = obj(value);
454
+ return {
455
+ signedIn: m.signedIn === true,
456
+ ...typeof m.nickname === "string" ? { nickname: m.nickname } : {}
457
+ };
458
+ }
459
+ function sanitizeSlot(value) {
460
+ const s = obj(value);
461
+ return typeof s.id === "string" ? { id: s.id, context: s.context } : void 0;
462
+ }
463
+ function sanitizeTranslations(value) {
464
+ if (typeof value !== "object" || value === null) return void 0;
465
+ return Object.freeze(
466
+ Object.fromEntries(
467
+ Object.entries(value).filter(
468
+ ([k, v]) => typeof v === "string" && k !== "__proto__"
469
+ )
470
+ )
471
+ );
472
+ }
473
+ function sanitizeEvent(type, payload) {
474
+ const p = obj(payload);
475
+ switch (type) {
476
+ case "theme":
477
+ return sanitizeTheme(p);
478
+ case "member":
479
+ return sanitizeMember(p);
480
+ case "visibility":
481
+ return { visible: p.visible === true };
482
+ case "slot.context": {
483
+ const slot = sanitizeSlot(p);
484
+ if (!slot) throw new TypeError("[bstage-bridge] slot.context\uC5D0\uB294 id\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
485
+ return slot;
486
+ }
487
+ case "locale": {
488
+ if (typeof p.locale !== "string") {
489
+ throw new TypeError("[bstage-bridge] locale \uC774\uBCA4\uD2B8\uC5D0\uB294 locale\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
490
+ }
491
+ const translations = sanitizeTranslations(p.translations);
492
+ return { locale: p.locale, ...translations ? { translations } : {} };
493
+ }
494
+ default:
495
+ throw new TypeError(`[bstage-bridge] \uC54C \uC218 \uC5C6\uB294 \uC774\uBCA4\uD2B8: ${String(type)}`);
496
+ }
497
+ }
498
+ function buildInit(init, hostOrigin, partnerOrigins, policy) {
499
+ const slot = init.slot ? sanitizeSlot(init.slot) : void 0;
500
+ const translations = sanitizeTranslations(init.translations);
501
+ return Object.freeze({
502
+ placementId: String(init.placementId),
503
+ surface: init.surface,
504
+ phase: String(init.phase),
505
+ spaceId: String(init.spaceId),
506
+ hostOrigin,
507
+ gatewayBase: `${hostOrigin}/gw`,
508
+ locale: String(init.locale),
509
+ theme: sanitizeTheme(init.theme),
510
+ ...slot ? { slot } : {},
511
+ member: sanitizeMember(init.member),
512
+ limits: {
513
+ maxBodyBytes: policy.maxBodyBytes,
514
+ maxRequestsPerMinute: policy.maxRequestsPerMinute
515
+ },
516
+ partnerOrigins,
517
+ ...translations ? { translations } : {}
518
+ });
519
+ }
520
+ function applyEvent(init, type, payload) {
521
+ switch (type) {
522
+ case "locale": {
523
+ const p = payload;
524
+ const { translations: _drop, ...rest } = init;
525
+ return Object.freeze({
526
+ ...rest,
527
+ locale: p.locale,
528
+ ...p.translations ? { translations: p.translations } : {}
529
+ });
530
+ }
531
+ case "member":
532
+ return Object.freeze({ ...init, member: payload });
533
+ case "theme":
534
+ return Object.freeze({ ...init, theme: payload });
535
+ case "slot.context":
536
+ return Object.freeze({ ...init, slot: payload });
537
+ default:
538
+ return init;
539
+ }
540
+ }
541
+
542
+ // src/host/storage.ts
543
+ var STORAGE_MAX_KEYS = 50;
544
+ var STORAGE_MAX_VALUE_LENGTH = 4096;
545
+ var STORAGE_MAX_KEY_LENGTH = 200;
546
+ var stores = /* @__PURE__ */ new Map();
547
+ function handleStorage(scope, req) {
548
+ const { op, key, value } = req;
549
+ if (typeof key !== "string" || key.length === 0 || key.length > STORAGE_MAX_KEY_LENGTH) {
550
+ throw new BridgeError("BAD_REQUEST", "invalid storage key");
551
+ }
552
+ const current = stores.get(scope) ?? {};
553
+ const has = Object.prototype.hasOwnProperty.call(current, key);
554
+ if (op === "get") return { value: has ? current[key] : null };
555
+ if (op === "remove") {
556
+ if (has) {
557
+ const next = Object.fromEntries(Object.entries(current).filter(([k]) => k !== key));
558
+ stores = new Map([...stores, [scope, next]]);
559
+ }
560
+ return { value: null };
561
+ }
562
+ if (op === "set") {
563
+ if (typeof value !== "string" || value.length > STORAGE_MAX_VALUE_LENGTH) {
564
+ throw new BridgeError("BAD_REQUEST", "invalid storage value");
565
+ }
566
+ if (!has && Object.keys(current).length >= STORAGE_MAX_KEYS) {
567
+ throw new BridgeError("BAD_REQUEST", "storage key limit reached");
568
+ }
569
+ stores = new Map([...stores, [scope, { ...current, [key]: value }]]);
570
+ return { value };
571
+ }
572
+ throw new BridgeError("BAD_REQUEST", "invalid storage op");
573
+ }
574
+
575
+ // src/host/validators.ts
576
+ var MAX_PATH_LENGTH = 2048;
577
+ var TOAST_MAX_LENGTH = 500;
578
+ var TRACK_NAME_MAX_LENGTH = 100;
579
+ var TRACK_PARAMS_MAX_BYTES = 4096;
580
+ var RESIZE_MAX_HEIGHT = 2e4;
581
+ var LOG_MAX_LENGTH = 2e3;
582
+ var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
583
+ var TOAST_KINDS = ["info", "success", "warning", "error"];
584
+ var LOG_LEVELS = ["debug", "info", "warn", "error"];
585
+ function field(payload, key) {
586
+ return typeof payload === "object" && payload !== null ? payload[key] : void 0;
587
+ }
588
+ function validatePath(payload, hostOrigin) {
589
+ const path = field(payload, "path");
590
+ if (typeof path !== "string" || path.length === 0 || path.length > MAX_PATH_LENGTH || !path.startsWith("/") || path.startsWith("//") || // eslint-disable-next-line no-control-regex -- 제어문자를 거르는 것이 목적이다
591
+ /[\s\\\u0000-\u001f\u007f]/.test(path)) {
592
+ throw new BridgeError("BAD_REQUEST", "invalid path");
593
+ }
594
+ let resolved;
595
+ try {
596
+ resolved = new URL(path, hostOrigin);
597
+ } catch {
598
+ throw new BridgeError("BAD_REQUEST", "invalid path");
599
+ }
600
+ if (resolved.origin !== new URL(hostOrigin).origin || resolved.pathname.startsWith("//")) {
601
+ throw new BridgeError("BAD_REQUEST", "invalid path");
602
+ }
603
+ if (!checkPath(resolved.pathname, { forbidDoubleSlash: true }).ok) {
604
+ throw new BridgeError("BAD_REQUEST", "invalid path");
605
+ }
606
+ return resolved.pathname + resolved.search + resolved.hash;
607
+ }
608
+ function validateExternalUrl(payload) {
609
+ const raw = field(payload, "url");
610
+ if (typeof raw !== "string") throw new BridgeError("BAD_REQUEST", "invalid url");
611
+ let url;
612
+ try {
613
+ url = new URL(raw);
614
+ } catch {
615
+ throw new BridgeError("BAD_REQUEST", "invalid url");
616
+ }
617
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
618
+ throw new BridgeError("FORBIDDEN_DESTINATION", "only https urls can be opened");
619
+ }
620
+ return url.href;
621
+ }
622
+ function validateToast(payload) {
623
+ const message = field(payload, "message");
624
+ if (typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid toast");
625
+ const kind = field(payload, "kind");
626
+ const known = TOAST_KINDS.find((k) => k === kind);
627
+ return { message: message.slice(0, TOAST_MAX_LENGTH), ...known ? { kind: known } : {} };
628
+ }
629
+ function validateHeight(payload) {
630
+ const height = field(payload, "height");
631
+ if (typeof height !== "number" || !Number.isFinite(height)) {
632
+ throw new BridgeError("BAD_REQUEST", "invalid height");
633
+ }
634
+ return Math.min(RESIZE_MAX_HEIGHT, Math.max(0, Math.round(height)));
635
+ }
636
+ function validateTrack(payload) {
637
+ const name = field(payload, "name");
638
+ const params = field(payload, "params");
639
+ if (typeof name !== "string" || name.length === 0 || name.length > TRACK_NAME_MAX_LENGTH) {
640
+ throw new BridgeError("BAD_REQUEST", "invalid track name");
641
+ }
642
+ if (params === void 0) return { name };
643
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
644
+ throw new BridgeError("BAD_REQUEST", "invalid track params");
645
+ }
646
+ let serialized;
647
+ try {
648
+ serialized = JSON.stringify(params);
649
+ } catch {
650
+ throw new BridgeError("BAD_REQUEST", "invalid track params");
651
+ }
652
+ if (new TextEncoder().encode(serialized).length > TRACK_PARAMS_MAX_BYTES) {
653
+ throw new BridgeError("BAD_REQUEST", "track params too large");
654
+ }
655
+ return {
656
+ name,
657
+ params: JSON.parse(
658
+ serialized,
659
+ (key, value) => UNSAFE_KEYS.includes(key) ? void 0 : value
660
+ )
661
+ };
662
+ }
663
+ function validateLog(payload) {
664
+ const level = field(payload, "level");
665
+ const message = field(payload, "message");
666
+ const known = LOG_LEVELS.find((l) => l === level);
667
+ if (!known || typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid log");
668
+ return { level: known, message: message.slice(0, LOG_MAX_LENGTH) };
669
+ }
670
+
671
+ // src/host/createBridgeHost.ts
672
+ var OPAQUE_ORIGIN = "null";
673
+ var UNLIMITED_REQUESTS = ["resize"];
674
+ var EMPTY = Object.freeze({});
675
+ var MAX_IN_FLIGHT_FETCHES = 16;
676
+ var TOAST_PER_MINUTE = 5;
677
+ var READY_PER_SECOND = 2;
678
+ var RESIZE_PER_SECOND = 30;
679
+ var defaultUncredentialedFetch = (url, init) => globalThis.fetch(url, {
680
+ ...init,
681
+ credentials: "omit",
682
+ redirect: "error",
683
+ referrerPolicy: "no-referrer"
684
+ });
685
+ function unsupported() {
686
+ throw new BridgeError("UNSUPPORTED", "not supported by host");
687
+ }
688
+ var TOO_LARGE = () => new BridgeError("BODY_TOO_LARGE", "response too large");
689
+ async function readBodyCapped(res, max) {
690
+ const stream = res.body;
691
+ if (stream === null) {
692
+ try {
693
+ return await res.text();
694
+ } catch {
695
+ throw new BridgeError("NETWORK_ERROR", "network error");
696
+ }
697
+ }
698
+ if (typeof stream?.getReader !== "function") {
699
+ throw new BridgeError("NETWORK_ERROR", "network error");
700
+ }
701
+ const reader = stream.getReader();
702
+ const decoder = new TextDecoder();
703
+ let received = 0;
704
+ let text = "";
705
+ try {
706
+ for (; ; ) {
707
+ const { done, value } = await reader.read();
708
+ if (done) break;
709
+ received += value.byteLength;
710
+ if (received > max) {
711
+ void reader.cancel().catch(() => {
712
+ });
713
+ throw TOO_LARGE();
714
+ }
715
+ text += decoder.decode(value, { stream: true });
716
+ }
717
+ } catch (err) {
718
+ if (err instanceof BridgeError) throw err;
719
+ throw new BridgeError("NETWORK_ERROR", "network error");
720
+ }
721
+ return text + decoder.decode();
722
+ }
723
+ async function runFetch(ctx, payload) {
724
+ const prepared = prepareFetch(payload, ctx.destination);
725
+ let res;
726
+ try {
727
+ const send = prepared.kind === "gateway" ? ctx.fetchImpl : prepared.kind === "partner" ? ctx.partnerFetchImpl : ctx.externalFetchImpl;
728
+ res = await send(prepared.url, {
729
+ ...prepared.init,
730
+ headers: { ...prepared.init.headers }
731
+ });
732
+ } catch {
733
+ throw new BridgeError("NETWORK_ERROR", "network error");
734
+ }
735
+ const max = ctx.destination.policy.maxResponseBytes;
736
+ const declared = Number(res.headers.get("content-length"));
737
+ if (Number.isFinite(declared) && declared > max) {
738
+ void res.body?.cancel().catch(() => {
739
+ });
740
+ throw TOO_LARGE();
741
+ }
742
+ if (prepared.init.method === "HEAD") {
743
+ void res.body?.cancel().catch(() => {
744
+ });
745
+ }
746
+ const bodyText = prepared.init.method === "HEAD" ? "" : await readBodyCapped(res, max);
747
+ const contentType = res.headers.get("content-type");
748
+ return {
749
+ status: res.status,
750
+ statusText: res.statusText,
751
+ headers: contentType !== null ? { "content-type": contentType } : {},
752
+ bodyText
753
+ };
754
+ }
755
+ function writeConsole(level, message) {
756
+ const write = globalThis.console?.[level];
757
+ if (typeof write === "function") write.call(globalThis.console, `[bstage-bridge] ${message}`);
758
+ }
759
+ async function dispatch(ctx, type, payload) {
760
+ const h = ctx.handlers;
761
+ switch (type) {
762
+ case "fetch":
763
+ return runFetch(ctx, payload);
764
+ case "navigate": {
765
+ const path = validatePath(payload, ctx.hostOrigin);
766
+ if (!h.navigate) unsupported();
767
+ h.navigate(path);
768
+ return EMPTY;
769
+ }
770
+ case "back":
771
+ if (!h.back) unsupported();
772
+ h.back();
773
+ return EMPTY;
774
+ case "openExternal": {
775
+ const url = validateExternalUrl(payload);
776
+ if (h.openExternal) h.openExternal(url);
777
+ else if (typeof ctx.win.open === "function")
778
+ ctx.win.open(url, "_blank", "noopener,noreferrer");
779
+ else unsupported();
780
+ return EMPTY;
781
+ }
782
+ case "toast": {
783
+ const { message, kind } = validateToast(payload);
784
+ if (!h.toast) unsupported();
785
+ if (!ctx.toastLimiter.take()) throw new BridgeError("RATE_LIMITED", "too many toasts");
786
+ h.toast(message, kind);
787
+ return EMPTY;
788
+ }
789
+ case "resize":
790
+ ctx.scheduleResize(validateHeight(payload));
791
+ return EMPTY;
792
+ case "track": {
793
+ const { name, params } = validateTrack(payload);
794
+ if (!h.track) unsupported();
795
+ h.track(name, params);
796
+ return EMPTY;
797
+ }
798
+ case "storage":
799
+ return handleStorage(ctx.storageScope, payload ?? {});
800
+ case "log": {
801
+ const { level, message } = validateLog(payload);
802
+ if (ctx.phase === "real") return EMPTY;
803
+ if (h.log) h.log(level, message);
804
+ else writeConsole(level, message);
805
+ return EMPTY;
806
+ }
807
+ }
808
+ }
809
+ function failure(code, message) {
810
+ return { ok: false, error: { code, message } };
811
+ }
812
+ function toFailure(err) {
813
+ if (err instanceof BridgeError || isBridgeErrorLike(err)) return failure(err.code, err.message);
814
+ return failure("BAD_REQUEST", "request failed");
815
+ }
816
+ function assertOptions(options) {
817
+ if (!options || typeof options !== "object") {
818
+ throw new TypeError("[bstage-bridge] createBridgeHost \uC635\uC158\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
819
+ }
820
+ if (!isValidSession(options.session)) {
821
+ throw new TypeError("[bstage-bridge] session \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4(createSession() \uC0AC\uC6A9)");
822
+ }
823
+ const hostOrigin = normalizeExactOrigin(options.hostOrigin, ["https:", "http:"]);
824
+ if (!hostOrigin) throw new TypeError("[bstage-bridge] hostOrigin\uC740 \uC815\uD655\uD55C origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
825
+ if (typeof options.fetchImpl !== "function") {
826
+ throw new TypeError("[bstage-bridge] fetchImpl\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
827
+ }
828
+ if (options.partnerFetchImpl !== void 0 && typeof options.partnerFetchImpl !== "function") {
829
+ throw new TypeError("[bstage-bridge] partnerFetchImpl\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
830
+ }
831
+ if (options.externalFetchImpl !== void 0 && typeof options.externalFetchImpl !== "function") {
832
+ throw new TypeError("[bstage-bridge] externalFetchImpl\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
833
+ }
834
+ if (options.externalFetchImpl !== void 0 && options.externalFetchImpl === options.fetchImpl) {
835
+ throw new TypeError(
836
+ "[bstage-bridge] externalFetchImpl\uC5D0 fetchImpl(\uC778\uC99D fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
837
+ );
838
+ }
839
+ if (options.partnerFetchImpl !== void 0 && options.partnerFetchImpl === options.fetchImpl) {
840
+ throw new TypeError("[bstage-bridge] partnerFetchImpl\uC5D0 fetchImpl(\uC778\uC99D fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
841
+ }
842
+ if (options.externalFetchImpl !== void 0 && options.externalFetchImpl === options.partnerFetchImpl) {
843
+ throw new TypeError(
844
+ "[bstage-bridge] externalFetchImpl\uC5D0 partnerFetchImpl(\uD32C \uD1A0\uD070 fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
845
+ );
846
+ }
847
+ if (!options.iframe || !options.init) {
848
+ throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
849
+ }
850
+ if (!isBridgeSurface(options.init.surface)) {
851
+ throw new TypeError('[bstage-bridge] init.surface\uB294 "page" \uB610\uB294 "slot"\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
852
+ }
853
+ return hostOrigin;
854
+ }
855
+ function createBridgeHost(options) {
856
+ const hostOrigin = assertOptions(options);
857
+ const policy = resolvePolicy(options.policy);
858
+ const partnerOrigins = normalizePartnerOrigins(options.init.partnerOrigins, hostOrigin);
859
+ const pinnedHeaders = normalizePinnedHeaders(options.pinnedHeaders);
860
+ const { iframe, session } = options;
861
+ const win = iframe.ownerDocument?.defaultView ?? globalThis;
862
+ const handlers = options.handlers ?? {};
863
+ const limiter = createRateLimiter(policy.maxRequestsPerMinute);
864
+ const readyLimiter = createRateLimiter(READY_PER_SECOND, 1e3);
865
+ const resizeLimiter = createRateLimiter(RESIZE_PER_SECOND, 1e3);
866
+ let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
867
+ let inFlightFetches = 0;
868
+ let disposed = false;
869
+ let resizeHandle;
870
+ let pendingHeight;
871
+ const raf = typeof win.requestAnimationFrame === "function" ? (cb) => win.requestAnimationFrame(cb) : (cb) => setTimeout(cb, 16);
872
+ const cancelRaf = typeof win.cancelAnimationFrame === "function" ? (id) => win.cancelAnimationFrame(id) : (id) => clearTimeout(id);
873
+ function scheduleResize(height) {
874
+ pendingHeight = height;
875
+ if (resizeHandle !== void 0 || !handlers.resize) return;
876
+ resizeHandle = raf(() => {
877
+ resizeHandle = void 0;
878
+ const h = pendingHeight;
879
+ pendingHeight = void 0;
880
+ if (!disposed && h !== void 0) handlers.resize?.(h);
881
+ });
882
+ }
883
+ const ctx = {
884
+ options,
885
+ handlers,
886
+ win,
887
+ destination: { hostOrigin, partnerOrigins, policy, pinnedHeaders },
888
+ storageScope: `${options.init.spaceId}\0${options.init.placementId}`,
889
+ phase: options.init.phase,
890
+ fetchImpl: options.fetchImpl,
891
+ partnerFetchImpl: options.partnerFetchImpl ?? defaultUncredentialedFetch,
892
+ externalFetchImpl: options.externalFetchImpl ?? defaultUncredentialedFetch,
893
+ hostOrigin,
894
+ toastLimiter: createRateLimiter(TOAST_PER_MINUTE),
895
+ scheduleResize
896
+ };
897
+ function post(target, envelope) {
898
+ if (disposed) return;
899
+ target.postMessage(envelope, "*");
900
+ }
901
+ async function handleRequest(target, env) {
902
+ let result;
903
+ if (env.type === "resize" && !resizeLimiter.take()) {
904
+ return;
905
+ }
906
+ if (!isRequestType(env.type)) {
907
+ result = failure("UNSUPPORTED", "unknown request type");
908
+ } else if (env.type === "fetch" && inFlightFetches >= MAX_IN_FLIGHT_FETCHES) {
909
+ result = failure("RATE_LIMITED", "too many concurrent requests");
910
+ } else if (!UNLIMITED_REQUESTS.includes(env.type) && !limiter.take()) {
911
+ result = failure("RATE_LIMITED", "too many requests");
912
+ } else {
913
+ if (env.type === "fetch") inFlightFetches += 1;
914
+ try {
915
+ result = { ok: true, data: await dispatch(ctx, env.type, env.payload) };
916
+ } catch (err) {
917
+ result = toFailure(err);
918
+ } finally {
919
+ if (env.type === "fetch") inFlightFetches -= 1;
920
+ }
921
+ }
922
+ post(target, createEnvelope("res", env.type, env.id, result));
923
+ }
924
+ function onMessage(ev) {
925
+ if (disposed) return;
926
+ const frame = iframe.contentWindow;
927
+ if (!frame || ev.source !== frame) return;
928
+ if (ev.origin !== OPAQUE_ORIGIN) return;
929
+ const env = ev.data;
930
+ if (!isBridgeEnvelope(env)) return;
931
+ if (!hasSession(env.payload, session)) return;
932
+ if (env.kind === "evt" && env.type === READY_EVENT) {
933
+ if (!readyLimiter.take()) return;
934
+ post(frame, createEnvelope("res", INIT_RESPONSE, env.id, { ok: true, data: current }));
935
+ return;
936
+ }
937
+ if (env.kind === "req") void handleRequest(frame, env);
938
+ }
939
+ win.addEventListener("message", onMessage);
940
+ return {
941
+ send(type, payload) {
942
+ if (disposed) return;
943
+ if (!isHostEventType(type)) throw new TypeError(`[bstage-bridge] \uC54C \uC218 \uC5C6\uB294 \uC774\uBCA4\uD2B8: ${type}`);
944
+ if (typeof payload !== "object" || payload === null) {
945
+ throw new TypeError("[bstage-bridge] \uC774\uBCA4\uD2B8 payload\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4");
946
+ }
947
+ const clean = sanitizeEvent(type, payload);
948
+ current = applyEvent(current, type, clean);
949
+ const frame = iframe.contentWindow;
950
+ if (frame) post(frame, createEnvelope("evt", type, createSession(), clean));
951
+ },
952
+ dispose() {
953
+ if (disposed) return;
954
+ disposed = true;
955
+ win.removeEventListener("message", onMessage);
956
+ if (resizeHandle !== void 0) cancelRaf(resizeHandle);
957
+ resizeHandle = void 0;
958
+ }
959
+ };
960
+ }
961
+
962
+ // src/frame/runtime.ts
963
+ function bridgeRuntime(g, makeFetch, config) {
964
+ const NS = "bstage-bridge";
965
+ const TIMEOUT_MS = 15e3;
966
+ const READY_INTERVAL_MS = 250;
967
+ const READY_FAST_ATTEMPTS = 40;
968
+ const READY_SLOW_INTERVAL_MS = 2e3;
969
+ const MAX_ID = 64;
970
+ const MAX_TYPE = 32;
971
+ if (g.__bstage_bridge__) return g.__bstage_bridge__;
972
+ const session = config.session;
973
+ const hostOrigin = config.hostOrigin;
974
+ const parent = g.parent;
975
+ const pending = /* @__PURE__ */ new Map();
976
+ const listeners = /* @__PURE__ */ new Map();
977
+ let init;
978
+ let seq = 0;
979
+ function randomPrefix() {
980
+ const bytes = new Uint8Array(8);
981
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
982
+ g.crypto.getRandomValues(bytes);
983
+ } else {
984
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
985
+ }
986
+ let out = "";
987
+ for (let i = 0; i < bytes.length; i += 1) out += (bytes[i] + 256).toString(16).slice(1);
988
+ return out;
989
+ }
990
+ const idPrefix = randomPrefix();
991
+ function nextId() {
992
+ seq += 1;
993
+ return idPrefix + "-" + seq;
994
+ }
995
+ function codeError(code, message) {
996
+ const err = new Error(message);
997
+ err.name = "BridgeError";
998
+ err.code = code;
999
+ return err;
1000
+ }
1001
+ function post(kind, type, id, payload) {
1002
+ if (!parent) return;
1003
+ const body = Object.assign({}, payload, { session });
1004
+ parent.postMessage({ v: 1, ns: NS, id, kind, type, payload: body }, hostOrigin);
1005
+ }
1006
+ function isEnvelope(m) {
1007
+ if (typeof m !== "object" || m === null) return false;
1008
+ const e = m;
1009
+ return e.v === 1 && e.ns === NS && typeof e.id === "string" && e.id.length > 0 && e.id.length <= MAX_ID && (e.kind === "req" || e.kind === "res" || e.kind === "evt") && typeof e.type === "string" && e.type.length > 0 && e.type.length <= MAX_TYPE && "payload" in e;
1010
+ }
1011
+ let resolveReady = function() {
1012
+ };
1013
+ const ready = new Promise(function(resolve) {
1014
+ resolveReady = resolve;
1015
+ });
1016
+ function emit(type, payload) {
1017
+ const fns = listeners.get(type);
1018
+ if (!fns) return;
1019
+ fns.slice().forEach(function(fn) {
1020
+ try {
1021
+ fn(payload);
1022
+ } catch {
1023
+ }
1024
+ });
1025
+ }
1026
+ function applyEvent2(type, payload) {
1027
+ if (!init || typeof payload !== "object" || payload === null) return;
1028
+ const p = payload;
1029
+ if (type === "locale" && typeof p.locale === "string") {
1030
+ init = Object.assign({}, init, { locale: p.locale, translations: p.translations });
1031
+ } else if (type === "member" && typeof p.signedIn === "boolean") {
1032
+ init = Object.assign({}, init, { member: payload });
1033
+ } else if (type === "theme" && typeof p.dark === "boolean") {
1034
+ init = Object.assign({}, init, { theme: payload });
1035
+ } else if (type === "slot.context" && typeof p.id === "string") {
1036
+ init = Object.assign({}, init, { slot: payload });
1037
+ }
1038
+ }
1039
+ function onMessage(ev) {
1040
+ if (!parent || ev.source !== parent) return;
1041
+ if (ev.origin !== hostOrigin) return;
1042
+ const m = ev.data;
1043
+ if (!isEnvelope(m)) return;
1044
+ const result = m.payload;
1045
+ if (m.kind === "res" && m.type === "init") {
1046
+ if (!init && result && result.ok === true && result.data) {
1047
+ init = result.data;
1048
+ clearInterval(readyTimer);
1049
+ resolveReady(init);
1050
+ }
1051
+ return;
1052
+ }
1053
+ if (m.kind === "res") {
1054
+ const entry = pending.get(m.id);
1055
+ if (!entry) return;
1056
+ pending.delete(m.id);
1057
+ clearTimeout(entry.timer);
1058
+ if (result && result.ok === true) {
1059
+ entry.resolve(result.data);
1060
+ } else {
1061
+ const error = result && result.error || {};
1062
+ entry.reject(
1063
+ codeError(
1064
+ typeof error.code === "string" ? error.code : "BAD_REQUEST",
1065
+ typeof error.message === "string" ? error.message : "bridge request failed"
1066
+ )
1067
+ );
1068
+ }
1069
+ return;
1070
+ }
1071
+ if (m.kind === "evt") {
1072
+ applyEvent2(m.type, m.payload);
1073
+ emit(m.type, m.payload);
1074
+ }
1075
+ }
1076
+ g.addEventListener("message", onMessage);
1077
+ let readyAttempts = 0;
1078
+ function sendReady() {
1079
+ readyAttempts += 1;
1080
+ if (readyAttempts === READY_FAST_ATTEMPTS) {
1081
+ clearInterval(readyTimer);
1082
+ readyTimer = setInterval(sendReady, READY_SLOW_INTERVAL_MS);
1083
+ }
1084
+ post("evt", "ready", nextId(), {});
1085
+ }
1086
+ let readyTimer = setInterval(sendReady, READY_INTERVAL_MS);
1087
+ sendReady();
1088
+ function request(type, payload) {
1089
+ return new Promise(function(resolve, reject) {
1090
+ const id = nextId();
1091
+ const timer = setTimeout(function() {
1092
+ pending.delete(id);
1093
+ reject(codeError("TIMEOUT", "bridge request timed out: " + type));
1094
+ }, TIMEOUT_MS);
1095
+ pending.set(id, { resolve, reject, timer });
1096
+ post("req", type, id, payload || {});
1097
+ });
1098
+ }
1099
+ function on(type, fn) {
1100
+ const current = listeners.get(type) || [];
1101
+ listeners.set(type, current.concat([fn]));
1102
+ return function() {
1103
+ const now = listeners.get(type) || [];
1104
+ listeners.set(
1105
+ type,
1106
+ now.filter(function(f) {
1107
+ return f !== fn;
1108
+ })
1109
+ );
1110
+ };
1111
+ }
1112
+ const base = {
1113
+ version: 1,
1114
+ hostOrigin,
1115
+ ready,
1116
+ getInit: function() {
1117
+ return init;
1118
+ },
1119
+ request,
1120
+ on
1121
+ };
1122
+ const client = Object.freeze(Object.assign({}, base, { fetch: makeFetch(base) }));
1123
+ Object.defineProperty(g, "__bstage_bridge__", {
1124
+ value: client,
1125
+ writable: false,
1126
+ configurable: false,
1127
+ enumerable: false
1128
+ });
1129
+ return client;
1130
+ }
1131
+
1132
+ // src/frame/createBridgeFetch.ts
1133
+ function createBridgeFetch(client) {
1134
+ const NULL_BODY_STATUS = [204, 205, 304];
1135
+ function toHeaderRecord(h) {
1136
+ const out = {};
1137
+ if (!h) return out;
1138
+ new Headers(h).forEach(function(value, key) {
1139
+ out[key] = value;
1140
+ });
1141
+ return out;
1142
+ }
1143
+ function abortError(signal) {
1144
+ if (signal.reason !== void 0) return signal.reason;
1145
+ return new DOMException("The operation was aborted.", "AbortError");
1146
+ }
1147
+ function bridgeFetch(input, init) {
1148
+ return new Promise(function(resolve, reject) {
1149
+ const opts = init || {};
1150
+ const isRequest = typeof input === "object" && input !== null && typeof input.method === "string";
1151
+ const url = isRequest ? input.url : String(input);
1152
+ const method = String(opts.method || (isRequest ? input.method : "GET"));
1153
+ const headers = toHeaderRecord(
1154
+ opts.headers || (isRequest ? input.headers : null)
1155
+ );
1156
+ let body = null;
1157
+ if (isRequest && input.body !== null && opts.body == null) {
1158
+ reject(new TypeError("bridge fetch: pass the body via init.body"));
1159
+ return;
1160
+ }
1161
+ const raw = opts.body;
1162
+ if (raw === void 0 || raw === null) {
1163
+ } else if (typeof raw === "string") {
1164
+ body = raw;
1165
+ } else if (typeof URLSearchParams !== "undefined" && raw instanceof URLSearchParams) {
1166
+ body = raw.toString();
1167
+ if (!headers["content-type"]) {
1168
+ headers["content-type"] = "application/x-www-form-urlencoded;charset=UTF-8";
1169
+ }
1170
+ } else {
1171
+ reject(new TypeError("bridge fetch: unsupported body type"));
1172
+ return;
1173
+ }
1174
+ const signal = opts.signal;
1175
+ if (signal && signal.aborted) {
1176
+ reject(abortError(signal));
1177
+ return;
1178
+ }
1179
+ let settled = false;
1180
+ function onAbort() {
1181
+ if (settled || !signal) return;
1182
+ settled = true;
1183
+ reject(abortError(signal));
1184
+ }
1185
+ if (signal) signal.addEventListener("abort", onAbort);
1186
+ const payload = { url, method, headers, body };
1187
+ client.request("fetch", payload).then(
1188
+ function(res) {
1189
+ if (signal) signal.removeEventListener("abort", onAbort);
1190
+ if (settled) return;
1191
+ settled = true;
1192
+ try {
1193
+ const nullBody = method.toUpperCase() === "HEAD" || NULL_BODY_STATUS.indexOf(res.status) >= 0;
1194
+ const responseHeaders = {};
1195
+ if (res.headers && typeof res.headers["content-type"] === "string") {
1196
+ responseHeaders["content-type"] = res.headers["content-type"];
1197
+ }
1198
+ resolve(
1199
+ new Response(nullBody ? null : typeof res.bodyText === "string" ? res.bodyText : "", {
1200
+ status: res.status,
1201
+ statusText: res.statusText,
1202
+ headers: responseHeaders
1203
+ })
1204
+ );
1205
+ } catch {
1206
+ reject(new TypeError("bridge fetch: invalid response"));
1207
+ }
1208
+ },
1209
+ function(err) {
1210
+ if (signal) signal.removeEventListener("abort", onAbort);
1211
+ if (settled) return;
1212
+ settled = true;
1213
+ const code = err && typeof err === "object" ? err.code : void 0;
1214
+ const failure2 = new TypeError("bridge fetch failed: " + String(code || "NETWORK_ERROR"));
1215
+ failure2.code = code || "NETWORK_ERROR";
1216
+ reject(failure2);
1217
+ }
1218
+ );
1219
+ });
1220
+ }
1221
+ return bridgeFetch;
1222
+ }
1223
+
1224
+ // src/srcdoc/escape.ts
1225
+ var HTML_ESCAPES = {
1226
+ "&": "&amp;",
1227
+ "<": "&lt;",
1228
+ ">": "&gt;",
1229
+ '"': "&quot;",
1230
+ "'": "&#39;"
1231
+ };
1232
+ function escapeHtml(value) {
1233
+ return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
1234
+ }
1235
+ function toScriptLiteral(value) {
1236
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1237
+ }
1238
+
1239
+ // src/srcdoc/createSrcdoc.ts
1240
+ var MAX_MEDIA_ORIGINS = 20;
1241
+ var MAX_ELEMENT_NAME_LENGTH = 100;
1242
+ var ELEMENT_NAME_RE = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
1243
+ function isValidElementName(value) {
1244
+ return typeof value === "string" && value.length <= MAX_ELEMENT_NAME_LENGTH && ELEMENT_NAME_RE.test(value);
1245
+ }
1246
+ function assertOrigin(label, value) {
1247
+ const origin = normalizeExactOrigin(value, ["https:", "http:"]);
1248
+ if (!origin) throw new TypeError(`[bstage-bridge] ${label}\uB294 \uC815\uD655\uD55C origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
1249
+ return origin;
1250
+ }
1251
+ function assertEntryUrl(entryUrl, assetOrigin) {
1252
+ let parsed;
1253
+ try {
1254
+ parsed = new URL(entryUrl);
1255
+ } catch {
1256
+ throw new TypeError("[bstage-bridge] entryUrl\uC774 URL\uC774 \uC544\uB2D9\uB2C8\uB2E4");
1257
+ }
1258
+ if (parsed.origin !== assetOrigin || parsed.username || parsed.password) {
1259
+ throw new TypeError("[bstage-bridge] entryUrl\uC740 assetOrigin\uC5D0 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4");
1260
+ }
1261
+ return parsed.href;
1262
+ }
1263
+ function normalizeMediaOrigins(value) {
1264
+ if (value === void 0) return Object.freeze([]);
1265
+ if (!Array.isArray(value)) {
1266
+ throw new TypeError("[bstage-bridge] mediaOrigins\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
1267
+ }
1268
+ if (value.length > MAX_MEDIA_ORIGINS) {
1269
+ throw new TypeError(`[bstage-bridge] mediaOrigins\uB294 \uCD5C\uB300 ${MAX_MEDIA_ORIGINS}\uAC1C\uC785\uB2C8\uB2E4`);
1270
+ }
1271
+ const origins = value.map((o) => {
1272
+ const origin = normalizeExactOrigin(o, ["https:"]);
1273
+ if (!origin) {
1274
+ throw new TypeError("[bstage-bridge] mediaOrigins \uD56D\uBAA9\uC740 \uC815\uD655\uD55C https origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
1275
+ }
1276
+ return origin;
1277
+ });
1278
+ return Object.freeze([...new Set(origins)]);
1279
+ }
1280
+ function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
1281
+ const media = [assetOrigin, ...normalizeMediaOrigins(mediaOrigins)].join(" ");
1282
+ return [
1283
+ "default-src 'none'",
1284
+ `script-src 'nonce-${nonce}' ${assetOrigin}`,
1285
+ `style-src 'unsafe-inline' ${assetOrigin}`,
1286
+ `img-src ${media} data: blob:`,
1287
+ `font-src ${assetOrigin} data:`,
1288
+ `media-src ${media} blob:`,
1289
+ "connect-src 'none'",
1290
+ "frame-src 'none'",
1291
+ "form-action 'none'",
1292
+ "base-uri 'none'"
1293
+ ].join("; ");
1294
+ }
1295
+ function bootstrapScript(o) {
1296
+ return [
1297
+ "(function () {",
1298
+ '"use strict";',
1299
+ `var SESSION = ${toScriptLiteral(o.session)};`,
1300
+ `var HOST_ORIGIN = ${toScriptLiteral(o.hostOrigin)};`,
1301
+ `var ENTRY_URL = ${toScriptLiteral(o.entryUrl)};`,
1302
+ `var ELEMENT_NAME = ${toScriptLiteral(o.elementName)};`,
1303
+ `var bridgeRuntime = ${bridgeRuntime.toString()};`,
1304
+ `var createBridgeFetch = ${createBridgeFetch.toString()};`,
1305
+ "var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
1306
+ "var lastHeight = -1;",
1307
+ "var scheduled = false;",
1308
+ "function reportHeight() {",
1309
+ " scheduled = false;",
1310
+ " var h = Math.ceil(document.body.getBoundingClientRect().height);",
1311
+ " if (h === lastHeight) return;",
1312
+ " lastHeight = h;",
1313
+ " client.request('resize', { height: h }).catch(function () {});",
1314
+ "}",
1315
+ "function scheduleHeight() {",
1316
+ " if (scheduled) return;",
1317
+ " scheduled = true;",
1318
+ " requestAnimationFrame(reportHeight);",
1319
+ "}",
1320
+ "client.ready.then(function () {",
1321
+ " var script = document.createElement('script');",
1322
+ " script.src = ENTRY_URL;",
1323
+ " script.onerror = function () {",
1324
+ " client.request('log', { level: 'error', message: 'template entry failed to load' }).catch(function () {});",
1325
+ " };",
1326
+ " document.head.appendChild(script);",
1327
+ " customElements.whenDefined(ELEMENT_NAME).then(function () {",
1328
+ " document.body.appendChild(document.createElement(ELEMENT_NAME));",
1329
+ " if (typeof ResizeObserver === 'function') {",
1330
+ " new ResizeObserver(scheduleHeight).observe(document.body);",
1331
+ " }",
1332
+ " scheduleHeight();",
1333
+ " });",
1334
+ "}, function () {});",
1335
+ "})();"
1336
+ ].join("\n");
1337
+ }
1338
+ function createSrcdoc(options) {
1339
+ const { nonce, session, title, elementName } = options;
1340
+ if (!isValidNonce(nonce)) throw new TypeError("[bstage-bridge] nonce \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4");
1341
+ if (!isValidSession(session)) throw new TypeError("[bstage-bridge] session \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4");
1342
+ const hostOrigin = assertOrigin("hostOrigin", options.hostOrigin);
1343
+ const assetOrigin = assertOrigin("assetOrigin", options.assetOrigin);
1344
+ const entryUrl = assertEntryUrl(options.entryUrl, assetOrigin);
1345
+ if (!isValidElementName(elementName)) {
1346
+ throw new TypeError("[bstage-bridge] elementName\uC774 custom element \uC774\uB984\uC774 \uC544\uB2D9\uB2C8\uB2E4");
1347
+ }
1348
+ if (typeof title !== "string") throw new TypeError("[bstage-bridge] title\uC740 \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
1349
+ const mediaOrigins = normalizeMediaOrigins(options.mediaOrigins);
1350
+ const csp = buildFrameCsp(nonce, assetOrigin, mediaOrigins);
1351
+ const script = bootstrapScript({ session, hostOrigin, entryUrl, elementName });
1352
+ if (/<\/script|<!--/i.test(script)) {
1353
+ 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");
1354
+ }
1355
+ return [
1356
+ "<!doctype html>",
1357
+ "<html>",
1358
+ "<head>",
1359
+ '<meta charset="utf-8">',
1360
+ `<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">`,
1361
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
1362
+ `<title>${escapeHtml(title)}</title>`,
1363
+ // body 높이를 콘텐츠 높이로 잰다 — documentElement.scrollHeight는 뷰포트 이상이라 줄어들지 못한다.
1364
+ // flow-root는 자식 margin이 body 밖으로 접혀 나가 높이에서 빠지는 것을 막는다.
1365
+ "<style>html,body{margin:0;padding:0;background:transparent;height:auto}body{display:flow-root}</style>",
1366
+ `<script nonce="${escapeHtml(nonce)}">
1367
+ ${script}
1368
+ </script>`,
1369
+ "</head>",
1370
+ "<body></body>",
1371
+ "</html>"
1372
+ ].join("\n");
1373
+ }
1374
+ // Annotate the CommonJS export names for ESM import in node:
1375
+ 0 && (module.exports = {
1376
+ ALLOWED_METHODS,
1377
+ ALLOWED_REQUEST_HEADERS,
1378
+ BRIDGE_SURFACES,
1379
+ DEFAULT_GATEWAY_DENY_SEGMENTS,
1380
+ DEFAULT_POLICY,
1381
+ HOST_ONLY_HEADERS,
1382
+ IFRAME_ATTRIBUTES,
1383
+ MAX_DECODE_PASSES,
1384
+ MAX_PARTNER_ORIGINS,
1385
+ checkPath,
1386
+ classifyDestination,
1387
+ createBridgeHost,
1388
+ createNonce,
1389
+ createSession,
1390
+ createSrcdoc,
1391
+ isAllowedGatewayPath,
1392
+ isBridgeSurface,
1393
+ isValidSession,
1394
+ normalizePartnerOrigins,
1395
+ normalizePinnedHeaders,
1396
+ prepareFetch,
1397
+ resolveDestination,
1398
+ resolvePolicy
1399
+ });