@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/LICENSE +21 -0
- package/README.md +194 -2
- package/dist/chunk-6LV7HEN4.js +583 -0
- package/dist/chunk-KQMJY4ZH.js +266 -0
- package/dist/createSrcdoc-bOoxttPz.d.cts +52 -0
- package/dist/createSrcdoc-bOoxttPz.d.ts +52 -0
- package/dist/frame.cjs +306 -0
- package/dist/frame.d.cts +72 -0
- package/dist/frame.d.ts +72 -0
- package/dist/frame.js +21 -0
- package/dist/host.cjs +1399 -0
- package/dist/host.d.cts +262 -0
- package/dist/host.d.ts +262 -0
- package/dist/host.js +594 -0
- package/dist/index.cjs +673 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +63 -0
- package/dist/messages-9N0GFq5b.d.cts +186 -0
- package/dist/messages-9N0GFq5b.d.ts +186 -0
- package/package.json +68 -3
package/dist/host.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALLOWED_METHODS,
|
|
3
|
+
ALLOWED_REQUEST_HEADERS,
|
|
4
|
+
BRIDGE_SURFACES,
|
|
5
|
+
BridgeError,
|
|
6
|
+
DEFAULT_GATEWAY_DENY_SEGMENTS,
|
|
7
|
+
DEFAULT_POLICY,
|
|
8
|
+
HOST_ONLY_HEADERS,
|
|
9
|
+
IFRAME_ATTRIBUTES,
|
|
10
|
+
INIT_RESPONSE,
|
|
11
|
+
MAX_DECODE_PASSES,
|
|
12
|
+
MAX_PARTNER_ORIGINS,
|
|
13
|
+
READY_EVENT,
|
|
14
|
+
checkPath,
|
|
15
|
+
classifyDestination,
|
|
16
|
+
createEnvelope,
|
|
17
|
+
createNonce,
|
|
18
|
+
createSession,
|
|
19
|
+
createSrcdoc,
|
|
20
|
+
hasSession,
|
|
21
|
+
isAllowedGatewayPath,
|
|
22
|
+
isBridgeEnvelope,
|
|
23
|
+
isBridgeErrorLike,
|
|
24
|
+
isBridgeSurface,
|
|
25
|
+
isHostEventType,
|
|
26
|
+
isRequestType,
|
|
27
|
+
isValidSession,
|
|
28
|
+
normalizeExactOrigin,
|
|
29
|
+
normalizePartnerOrigins,
|
|
30
|
+
normalizePinnedHeaders,
|
|
31
|
+
prepareFetch,
|
|
32
|
+
resolveDestination,
|
|
33
|
+
resolvePolicy
|
|
34
|
+
} from "./chunk-6LV7HEN4.js";
|
|
35
|
+
import "./chunk-KQMJY4ZH.js";
|
|
36
|
+
|
|
37
|
+
// src/host/rateLimiter.ts
|
|
38
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
39
|
+
function createRateLimiter(max, windowMs = DEFAULT_WINDOW_MS) {
|
|
40
|
+
let stamps = [];
|
|
41
|
+
return {
|
|
42
|
+
take(now = Date.now()) {
|
|
43
|
+
const recent = stamps.filter((t) => now - t < windowMs);
|
|
44
|
+
if (recent.length >= max) {
|
|
45
|
+
stamps = recent;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
stamps = [...recent, now];
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/host/events.ts
|
|
55
|
+
function obj(value) {
|
|
56
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
57
|
+
}
|
|
58
|
+
function sanitizeTheme(value) {
|
|
59
|
+
return { dark: obj(value).dark === true };
|
|
60
|
+
}
|
|
61
|
+
function sanitizeMember(value) {
|
|
62
|
+
const m = obj(value);
|
|
63
|
+
return {
|
|
64
|
+
signedIn: m.signedIn === true,
|
|
65
|
+
...typeof m.nickname === "string" ? { nickname: m.nickname } : {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function sanitizeSlot(value) {
|
|
69
|
+
const s = obj(value);
|
|
70
|
+
return typeof s.id === "string" ? { id: s.id, context: s.context } : void 0;
|
|
71
|
+
}
|
|
72
|
+
function sanitizeTranslations(value) {
|
|
73
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
74
|
+
return Object.freeze(
|
|
75
|
+
Object.fromEntries(
|
|
76
|
+
Object.entries(value).filter(
|
|
77
|
+
([k, v]) => typeof v === "string" && k !== "__proto__"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function sanitizeEvent(type, payload) {
|
|
83
|
+
const p = obj(payload);
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "theme":
|
|
86
|
+
return sanitizeTheme(p);
|
|
87
|
+
case "member":
|
|
88
|
+
return sanitizeMember(p);
|
|
89
|
+
case "visibility":
|
|
90
|
+
return { visible: p.visible === true };
|
|
91
|
+
case "slot.context": {
|
|
92
|
+
const slot = sanitizeSlot(p);
|
|
93
|
+
if (!slot) throw new TypeError("[bstage-bridge] slot.context\uC5D0\uB294 id\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
94
|
+
return slot;
|
|
95
|
+
}
|
|
96
|
+
case "locale": {
|
|
97
|
+
if (typeof p.locale !== "string") {
|
|
98
|
+
throw new TypeError("[bstage-bridge] locale \uC774\uBCA4\uD2B8\uC5D0\uB294 locale\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
99
|
+
}
|
|
100
|
+
const translations = sanitizeTranslations(p.translations);
|
|
101
|
+
return { locale: p.locale, ...translations ? { translations } : {} };
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
throw new TypeError(`[bstage-bridge] \uC54C \uC218 \uC5C6\uB294 \uC774\uBCA4\uD2B8: ${String(type)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function buildInit(init, hostOrigin, partnerOrigins, policy) {
|
|
108
|
+
const slot = init.slot ? sanitizeSlot(init.slot) : void 0;
|
|
109
|
+
const translations = sanitizeTranslations(init.translations);
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
placementId: String(init.placementId),
|
|
112
|
+
surface: init.surface,
|
|
113
|
+
phase: String(init.phase),
|
|
114
|
+
spaceId: String(init.spaceId),
|
|
115
|
+
hostOrigin,
|
|
116
|
+
gatewayBase: `${hostOrigin}/gw`,
|
|
117
|
+
locale: String(init.locale),
|
|
118
|
+
theme: sanitizeTheme(init.theme),
|
|
119
|
+
...slot ? { slot } : {},
|
|
120
|
+
member: sanitizeMember(init.member),
|
|
121
|
+
limits: {
|
|
122
|
+
maxBodyBytes: policy.maxBodyBytes,
|
|
123
|
+
maxRequestsPerMinute: policy.maxRequestsPerMinute
|
|
124
|
+
},
|
|
125
|
+
partnerOrigins,
|
|
126
|
+
...translations ? { translations } : {}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function applyEvent(init, type, payload) {
|
|
130
|
+
switch (type) {
|
|
131
|
+
case "locale": {
|
|
132
|
+
const p = payload;
|
|
133
|
+
const { translations: _drop, ...rest } = init;
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
...rest,
|
|
136
|
+
locale: p.locale,
|
|
137
|
+
...p.translations ? { translations: p.translations } : {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
case "member":
|
|
141
|
+
return Object.freeze({ ...init, member: payload });
|
|
142
|
+
case "theme":
|
|
143
|
+
return Object.freeze({ ...init, theme: payload });
|
|
144
|
+
case "slot.context":
|
|
145
|
+
return Object.freeze({ ...init, slot: payload });
|
|
146
|
+
default:
|
|
147
|
+
return init;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/host/storage.ts
|
|
152
|
+
var STORAGE_MAX_KEYS = 50;
|
|
153
|
+
var STORAGE_MAX_VALUE_LENGTH = 4096;
|
|
154
|
+
var STORAGE_MAX_KEY_LENGTH = 200;
|
|
155
|
+
var stores = /* @__PURE__ */ new Map();
|
|
156
|
+
function handleStorage(scope, req) {
|
|
157
|
+
const { op, key, value } = req;
|
|
158
|
+
if (typeof key !== "string" || key.length === 0 || key.length > STORAGE_MAX_KEY_LENGTH) {
|
|
159
|
+
throw new BridgeError("BAD_REQUEST", "invalid storage key");
|
|
160
|
+
}
|
|
161
|
+
const current = stores.get(scope) ?? {};
|
|
162
|
+
const has = Object.prototype.hasOwnProperty.call(current, key);
|
|
163
|
+
if (op === "get") return { value: has ? current[key] : null };
|
|
164
|
+
if (op === "remove") {
|
|
165
|
+
if (has) {
|
|
166
|
+
const next = Object.fromEntries(Object.entries(current).filter(([k]) => k !== key));
|
|
167
|
+
stores = new Map([...stores, [scope, next]]);
|
|
168
|
+
}
|
|
169
|
+
return { value: null };
|
|
170
|
+
}
|
|
171
|
+
if (op === "set") {
|
|
172
|
+
if (typeof value !== "string" || value.length > STORAGE_MAX_VALUE_LENGTH) {
|
|
173
|
+
throw new BridgeError("BAD_REQUEST", "invalid storage value");
|
|
174
|
+
}
|
|
175
|
+
if (!has && Object.keys(current).length >= STORAGE_MAX_KEYS) {
|
|
176
|
+
throw new BridgeError("BAD_REQUEST", "storage key limit reached");
|
|
177
|
+
}
|
|
178
|
+
stores = new Map([...stores, [scope, { ...current, [key]: value }]]);
|
|
179
|
+
return { value };
|
|
180
|
+
}
|
|
181
|
+
throw new BridgeError("BAD_REQUEST", "invalid storage op");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/host/validators.ts
|
|
185
|
+
var MAX_PATH_LENGTH = 2048;
|
|
186
|
+
var TOAST_MAX_LENGTH = 500;
|
|
187
|
+
var TRACK_NAME_MAX_LENGTH = 100;
|
|
188
|
+
var TRACK_PARAMS_MAX_BYTES = 4096;
|
|
189
|
+
var RESIZE_MAX_HEIGHT = 2e4;
|
|
190
|
+
var LOG_MAX_LENGTH = 2e3;
|
|
191
|
+
var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
192
|
+
var TOAST_KINDS = ["info", "success", "warning", "error"];
|
|
193
|
+
var LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
194
|
+
function field(payload, key) {
|
|
195
|
+
return typeof payload === "object" && payload !== null ? payload[key] : void 0;
|
|
196
|
+
}
|
|
197
|
+
function validatePath(payload, hostOrigin) {
|
|
198
|
+
const path = field(payload, "path");
|
|
199
|
+
if (typeof path !== "string" || path.length === 0 || path.length > MAX_PATH_LENGTH || !path.startsWith("/") || path.startsWith("//") || // eslint-disable-next-line no-control-regex -- 제어문자를 거르는 것이 목적이다
|
|
200
|
+
/[\s\\\u0000-\u001f\u007f]/.test(path)) {
|
|
201
|
+
throw new BridgeError("BAD_REQUEST", "invalid path");
|
|
202
|
+
}
|
|
203
|
+
let resolved;
|
|
204
|
+
try {
|
|
205
|
+
resolved = new URL(path, hostOrigin);
|
|
206
|
+
} catch {
|
|
207
|
+
throw new BridgeError("BAD_REQUEST", "invalid path");
|
|
208
|
+
}
|
|
209
|
+
if (resolved.origin !== new URL(hostOrigin).origin || resolved.pathname.startsWith("//")) {
|
|
210
|
+
throw new BridgeError("BAD_REQUEST", "invalid path");
|
|
211
|
+
}
|
|
212
|
+
if (!checkPath(resolved.pathname, { forbidDoubleSlash: true }).ok) {
|
|
213
|
+
throw new BridgeError("BAD_REQUEST", "invalid path");
|
|
214
|
+
}
|
|
215
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
216
|
+
}
|
|
217
|
+
function validateExternalUrl(payload) {
|
|
218
|
+
const raw = field(payload, "url");
|
|
219
|
+
if (typeof raw !== "string") throw new BridgeError("BAD_REQUEST", "invalid url");
|
|
220
|
+
let url;
|
|
221
|
+
try {
|
|
222
|
+
url = new URL(raw);
|
|
223
|
+
} catch {
|
|
224
|
+
throw new BridgeError("BAD_REQUEST", "invalid url");
|
|
225
|
+
}
|
|
226
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
|
|
227
|
+
throw new BridgeError("FORBIDDEN_DESTINATION", "only https urls can be opened");
|
|
228
|
+
}
|
|
229
|
+
return url.href;
|
|
230
|
+
}
|
|
231
|
+
function validateToast(payload) {
|
|
232
|
+
const message = field(payload, "message");
|
|
233
|
+
if (typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid toast");
|
|
234
|
+
const kind = field(payload, "kind");
|
|
235
|
+
const known = TOAST_KINDS.find((k) => k === kind);
|
|
236
|
+
return { message: message.slice(0, TOAST_MAX_LENGTH), ...known ? { kind: known } : {} };
|
|
237
|
+
}
|
|
238
|
+
function validateHeight(payload) {
|
|
239
|
+
const height = field(payload, "height");
|
|
240
|
+
if (typeof height !== "number" || !Number.isFinite(height)) {
|
|
241
|
+
throw new BridgeError("BAD_REQUEST", "invalid height");
|
|
242
|
+
}
|
|
243
|
+
return Math.min(RESIZE_MAX_HEIGHT, Math.max(0, Math.round(height)));
|
|
244
|
+
}
|
|
245
|
+
function validateTrack(payload) {
|
|
246
|
+
const name = field(payload, "name");
|
|
247
|
+
const params = field(payload, "params");
|
|
248
|
+
if (typeof name !== "string" || name.length === 0 || name.length > TRACK_NAME_MAX_LENGTH) {
|
|
249
|
+
throw new BridgeError("BAD_REQUEST", "invalid track name");
|
|
250
|
+
}
|
|
251
|
+
if (params === void 0) return { name };
|
|
252
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
253
|
+
throw new BridgeError("BAD_REQUEST", "invalid track params");
|
|
254
|
+
}
|
|
255
|
+
let serialized;
|
|
256
|
+
try {
|
|
257
|
+
serialized = JSON.stringify(params);
|
|
258
|
+
} catch {
|
|
259
|
+
throw new BridgeError("BAD_REQUEST", "invalid track params");
|
|
260
|
+
}
|
|
261
|
+
if (new TextEncoder().encode(serialized).length > TRACK_PARAMS_MAX_BYTES) {
|
|
262
|
+
throw new BridgeError("BAD_REQUEST", "track params too large");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
name,
|
|
266
|
+
params: JSON.parse(
|
|
267
|
+
serialized,
|
|
268
|
+
(key, value) => UNSAFE_KEYS.includes(key) ? void 0 : value
|
|
269
|
+
)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function validateLog(payload) {
|
|
273
|
+
const level = field(payload, "level");
|
|
274
|
+
const message = field(payload, "message");
|
|
275
|
+
const known = LOG_LEVELS.find((l) => l === level);
|
|
276
|
+
if (!known || typeof message !== "string") throw new BridgeError("BAD_REQUEST", "invalid log");
|
|
277
|
+
return { level: known, message: message.slice(0, LOG_MAX_LENGTH) };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/host/createBridgeHost.ts
|
|
281
|
+
var OPAQUE_ORIGIN = "null";
|
|
282
|
+
var UNLIMITED_REQUESTS = ["resize"];
|
|
283
|
+
var EMPTY = Object.freeze({});
|
|
284
|
+
var MAX_IN_FLIGHT_FETCHES = 16;
|
|
285
|
+
var TOAST_PER_MINUTE = 5;
|
|
286
|
+
var READY_PER_SECOND = 2;
|
|
287
|
+
var RESIZE_PER_SECOND = 30;
|
|
288
|
+
var defaultUncredentialedFetch = (url, init) => globalThis.fetch(url, {
|
|
289
|
+
...init,
|
|
290
|
+
credentials: "omit",
|
|
291
|
+
redirect: "error",
|
|
292
|
+
referrerPolicy: "no-referrer"
|
|
293
|
+
});
|
|
294
|
+
function unsupported() {
|
|
295
|
+
throw new BridgeError("UNSUPPORTED", "not supported by host");
|
|
296
|
+
}
|
|
297
|
+
var TOO_LARGE = () => new BridgeError("BODY_TOO_LARGE", "response too large");
|
|
298
|
+
async function readBodyCapped(res, max) {
|
|
299
|
+
const stream = res.body;
|
|
300
|
+
if (stream === null) {
|
|
301
|
+
try {
|
|
302
|
+
return await res.text();
|
|
303
|
+
} catch {
|
|
304
|
+
throw new BridgeError("NETWORK_ERROR", "network error");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (typeof stream?.getReader !== "function") {
|
|
308
|
+
throw new BridgeError("NETWORK_ERROR", "network error");
|
|
309
|
+
}
|
|
310
|
+
const reader = stream.getReader();
|
|
311
|
+
const decoder = new TextDecoder();
|
|
312
|
+
let received = 0;
|
|
313
|
+
let text = "";
|
|
314
|
+
try {
|
|
315
|
+
for (; ; ) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done) break;
|
|
318
|
+
received += value.byteLength;
|
|
319
|
+
if (received > max) {
|
|
320
|
+
void reader.cancel().catch(() => {
|
|
321
|
+
});
|
|
322
|
+
throw TOO_LARGE();
|
|
323
|
+
}
|
|
324
|
+
text += decoder.decode(value, { stream: true });
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (err instanceof BridgeError) throw err;
|
|
328
|
+
throw new BridgeError("NETWORK_ERROR", "network error");
|
|
329
|
+
}
|
|
330
|
+
return text + decoder.decode();
|
|
331
|
+
}
|
|
332
|
+
async function runFetch(ctx, payload) {
|
|
333
|
+
const prepared = prepareFetch(payload, ctx.destination);
|
|
334
|
+
let res;
|
|
335
|
+
try {
|
|
336
|
+
const send = prepared.kind === "gateway" ? ctx.fetchImpl : prepared.kind === "partner" ? ctx.partnerFetchImpl : ctx.externalFetchImpl;
|
|
337
|
+
res = await send(prepared.url, {
|
|
338
|
+
...prepared.init,
|
|
339
|
+
headers: { ...prepared.init.headers }
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
throw new BridgeError("NETWORK_ERROR", "network error");
|
|
343
|
+
}
|
|
344
|
+
const max = ctx.destination.policy.maxResponseBytes;
|
|
345
|
+
const declared = Number(res.headers.get("content-length"));
|
|
346
|
+
if (Number.isFinite(declared) && declared > max) {
|
|
347
|
+
void res.body?.cancel().catch(() => {
|
|
348
|
+
});
|
|
349
|
+
throw TOO_LARGE();
|
|
350
|
+
}
|
|
351
|
+
if (prepared.init.method === "HEAD") {
|
|
352
|
+
void res.body?.cancel().catch(() => {
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const bodyText = prepared.init.method === "HEAD" ? "" : await readBodyCapped(res, max);
|
|
356
|
+
const contentType = res.headers.get("content-type");
|
|
357
|
+
return {
|
|
358
|
+
status: res.status,
|
|
359
|
+
statusText: res.statusText,
|
|
360
|
+
headers: contentType !== null ? { "content-type": contentType } : {},
|
|
361
|
+
bodyText
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function writeConsole(level, message) {
|
|
365
|
+
const write = globalThis.console?.[level];
|
|
366
|
+
if (typeof write === "function") write.call(globalThis.console, `[bstage-bridge] ${message}`);
|
|
367
|
+
}
|
|
368
|
+
async function dispatch(ctx, type, payload) {
|
|
369
|
+
const h = ctx.handlers;
|
|
370
|
+
switch (type) {
|
|
371
|
+
case "fetch":
|
|
372
|
+
return runFetch(ctx, payload);
|
|
373
|
+
case "navigate": {
|
|
374
|
+
const path = validatePath(payload, ctx.hostOrigin);
|
|
375
|
+
if (!h.navigate) unsupported();
|
|
376
|
+
h.navigate(path);
|
|
377
|
+
return EMPTY;
|
|
378
|
+
}
|
|
379
|
+
case "back":
|
|
380
|
+
if (!h.back) unsupported();
|
|
381
|
+
h.back();
|
|
382
|
+
return EMPTY;
|
|
383
|
+
case "openExternal": {
|
|
384
|
+
const url = validateExternalUrl(payload);
|
|
385
|
+
if (h.openExternal) h.openExternal(url);
|
|
386
|
+
else if (typeof ctx.win.open === "function")
|
|
387
|
+
ctx.win.open(url, "_blank", "noopener,noreferrer");
|
|
388
|
+
else unsupported();
|
|
389
|
+
return EMPTY;
|
|
390
|
+
}
|
|
391
|
+
case "toast": {
|
|
392
|
+
const { message, kind } = validateToast(payload);
|
|
393
|
+
if (!h.toast) unsupported();
|
|
394
|
+
if (!ctx.toastLimiter.take()) throw new BridgeError("RATE_LIMITED", "too many toasts");
|
|
395
|
+
h.toast(message, kind);
|
|
396
|
+
return EMPTY;
|
|
397
|
+
}
|
|
398
|
+
case "resize":
|
|
399
|
+
ctx.scheduleResize(validateHeight(payload));
|
|
400
|
+
return EMPTY;
|
|
401
|
+
case "track": {
|
|
402
|
+
const { name, params } = validateTrack(payload);
|
|
403
|
+
if (!h.track) unsupported();
|
|
404
|
+
h.track(name, params);
|
|
405
|
+
return EMPTY;
|
|
406
|
+
}
|
|
407
|
+
case "storage":
|
|
408
|
+
return handleStorage(ctx.storageScope, payload ?? {});
|
|
409
|
+
case "log": {
|
|
410
|
+
const { level, message } = validateLog(payload);
|
|
411
|
+
if (ctx.phase === "real") return EMPTY;
|
|
412
|
+
if (h.log) h.log(level, message);
|
|
413
|
+
else writeConsole(level, message);
|
|
414
|
+
return EMPTY;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function failure(code, message) {
|
|
419
|
+
return { ok: false, error: { code, message } };
|
|
420
|
+
}
|
|
421
|
+
function toFailure(err) {
|
|
422
|
+
if (err instanceof BridgeError || isBridgeErrorLike(err)) return failure(err.code, err.message);
|
|
423
|
+
return failure("BAD_REQUEST", "request failed");
|
|
424
|
+
}
|
|
425
|
+
function assertOptions(options) {
|
|
426
|
+
if (!options || typeof options !== "object") {
|
|
427
|
+
throw new TypeError("[bstage-bridge] createBridgeHost \uC635\uC158\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
428
|
+
}
|
|
429
|
+
if (!isValidSession(options.session)) {
|
|
430
|
+
throw new TypeError("[bstage-bridge] session \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4(createSession() \uC0AC\uC6A9)");
|
|
431
|
+
}
|
|
432
|
+
const hostOrigin = normalizeExactOrigin(options.hostOrigin, ["https:", "http:"]);
|
|
433
|
+
if (!hostOrigin) throw new TypeError("[bstage-bridge] hostOrigin\uC740 \uC815\uD655\uD55C origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
434
|
+
if (typeof options.fetchImpl !== "function") {
|
|
435
|
+
throw new TypeError("[bstage-bridge] fetchImpl\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
436
|
+
}
|
|
437
|
+
if (options.partnerFetchImpl !== void 0 && typeof options.partnerFetchImpl !== "function") {
|
|
438
|
+
throw new TypeError("[bstage-bridge] partnerFetchImpl\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
439
|
+
}
|
|
440
|
+
if (options.externalFetchImpl !== void 0 && typeof options.externalFetchImpl !== "function") {
|
|
441
|
+
throw new TypeError("[bstage-bridge] externalFetchImpl\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
442
|
+
}
|
|
443
|
+
if (options.externalFetchImpl !== void 0 && options.externalFetchImpl === options.fetchImpl) {
|
|
444
|
+
throw new TypeError(
|
|
445
|
+
"[bstage-bridge] externalFetchImpl\uC5D0 fetchImpl(\uC778\uC99D fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (options.partnerFetchImpl !== void 0 && options.partnerFetchImpl === options.fetchImpl) {
|
|
449
|
+
throw new TypeError("[bstage-bridge] partnerFetchImpl\uC5D0 fetchImpl(\uC778\uC99D fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
450
|
+
}
|
|
451
|
+
if (options.externalFetchImpl !== void 0 && options.externalFetchImpl === options.partnerFetchImpl) {
|
|
452
|
+
throw new TypeError(
|
|
453
|
+
"[bstage-bridge] externalFetchImpl\uC5D0 partnerFetchImpl(\uD32C \uD1A0\uD070 fetch)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (!options.iframe || !options.init) {
|
|
457
|
+
throw new TypeError("[bstage-bridge] iframe\xB7init\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
458
|
+
}
|
|
459
|
+
if (!isBridgeSurface(options.init.surface)) {
|
|
460
|
+
throw new TypeError('[bstage-bridge] init.surface\uB294 "page" \uB610\uB294 "slot"\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
|
|
461
|
+
}
|
|
462
|
+
return hostOrigin;
|
|
463
|
+
}
|
|
464
|
+
function createBridgeHost(options) {
|
|
465
|
+
const hostOrigin = assertOptions(options);
|
|
466
|
+
const policy = resolvePolicy(options.policy);
|
|
467
|
+
const partnerOrigins = normalizePartnerOrigins(options.init.partnerOrigins, hostOrigin);
|
|
468
|
+
const pinnedHeaders = normalizePinnedHeaders(options.pinnedHeaders);
|
|
469
|
+
const { iframe, session } = options;
|
|
470
|
+
const win = iframe.ownerDocument?.defaultView ?? globalThis;
|
|
471
|
+
const handlers = options.handlers ?? {};
|
|
472
|
+
const limiter = createRateLimiter(policy.maxRequestsPerMinute);
|
|
473
|
+
const readyLimiter = createRateLimiter(READY_PER_SECOND, 1e3);
|
|
474
|
+
const resizeLimiter = createRateLimiter(RESIZE_PER_SECOND, 1e3);
|
|
475
|
+
let current = buildInit(options.init, hostOrigin, partnerOrigins, policy);
|
|
476
|
+
let inFlightFetches = 0;
|
|
477
|
+
let disposed = false;
|
|
478
|
+
let resizeHandle;
|
|
479
|
+
let pendingHeight;
|
|
480
|
+
const raf = typeof win.requestAnimationFrame === "function" ? (cb) => win.requestAnimationFrame(cb) : (cb) => setTimeout(cb, 16);
|
|
481
|
+
const cancelRaf = typeof win.cancelAnimationFrame === "function" ? (id) => win.cancelAnimationFrame(id) : (id) => clearTimeout(id);
|
|
482
|
+
function scheduleResize(height) {
|
|
483
|
+
pendingHeight = height;
|
|
484
|
+
if (resizeHandle !== void 0 || !handlers.resize) return;
|
|
485
|
+
resizeHandle = raf(() => {
|
|
486
|
+
resizeHandle = void 0;
|
|
487
|
+
const h = pendingHeight;
|
|
488
|
+
pendingHeight = void 0;
|
|
489
|
+
if (!disposed && h !== void 0) handlers.resize?.(h);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const ctx = {
|
|
493
|
+
options,
|
|
494
|
+
handlers,
|
|
495
|
+
win,
|
|
496
|
+
destination: { hostOrigin, partnerOrigins, policy, pinnedHeaders },
|
|
497
|
+
storageScope: `${options.init.spaceId}\0${options.init.placementId}`,
|
|
498
|
+
phase: options.init.phase,
|
|
499
|
+
fetchImpl: options.fetchImpl,
|
|
500
|
+
partnerFetchImpl: options.partnerFetchImpl ?? defaultUncredentialedFetch,
|
|
501
|
+
externalFetchImpl: options.externalFetchImpl ?? defaultUncredentialedFetch,
|
|
502
|
+
hostOrigin,
|
|
503
|
+
toastLimiter: createRateLimiter(TOAST_PER_MINUTE),
|
|
504
|
+
scheduleResize
|
|
505
|
+
};
|
|
506
|
+
function post(target, envelope) {
|
|
507
|
+
if (disposed) return;
|
|
508
|
+
target.postMessage(envelope, "*");
|
|
509
|
+
}
|
|
510
|
+
async function handleRequest(target, env) {
|
|
511
|
+
let result;
|
|
512
|
+
if (env.type === "resize" && !resizeLimiter.take()) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!isRequestType(env.type)) {
|
|
516
|
+
result = failure("UNSUPPORTED", "unknown request type");
|
|
517
|
+
} else if (env.type === "fetch" && inFlightFetches >= MAX_IN_FLIGHT_FETCHES) {
|
|
518
|
+
result = failure("RATE_LIMITED", "too many concurrent requests");
|
|
519
|
+
} else if (!UNLIMITED_REQUESTS.includes(env.type) && !limiter.take()) {
|
|
520
|
+
result = failure("RATE_LIMITED", "too many requests");
|
|
521
|
+
} else {
|
|
522
|
+
if (env.type === "fetch") inFlightFetches += 1;
|
|
523
|
+
try {
|
|
524
|
+
result = { ok: true, data: await dispatch(ctx, env.type, env.payload) };
|
|
525
|
+
} catch (err) {
|
|
526
|
+
result = toFailure(err);
|
|
527
|
+
} finally {
|
|
528
|
+
if (env.type === "fetch") inFlightFetches -= 1;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
post(target, createEnvelope("res", env.type, env.id, result));
|
|
532
|
+
}
|
|
533
|
+
function onMessage(ev) {
|
|
534
|
+
if (disposed) return;
|
|
535
|
+
const frame = iframe.contentWindow;
|
|
536
|
+
if (!frame || ev.source !== frame) return;
|
|
537
|
+
if (ev.origin !== OPAQUE_ORIGIN) return;
|
|
538
|
+
const env = ev.data;
|
|
539
|
+
if (!isBridgeEnvelope(env)) return;
|
|
540
|
+
if (!hasSession(env.payload, session)) return;
|
|
541
|
+
if (env.kind === "evt" && env.type === READY_EVENT) {
|
|
542
|
+
if (!readyLimiter.take()) return;
|
|
543
|
+
post(frame, createEnvelope("res", INIT_RESPONSE, env.id, { ok: true, data: current }));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (env.kind === "req") void handleRequest(frame, env);
|
|
547
|
+
}
|
|
548
|
+
win.addEventListener("message", onMessage);
|
|
549
|
+
return {
|
|
550
|
+
send(type, payload) {
|
|
551
|
+
if (disposed) return;
|
|
552
|
+
if (!isHostEventType(type)) throw new TypeError(`[bstage-bridge] \uC54C \uC218 \uC5C6\uB294 \uC774\uBCA4\uD2B8: ${type}`);
|
|
553
|
+
if (typeof payload !== "object" || payload === null) {
|
|
554
|
+
throw new TypeError("[bstage-bridge] \uC774\uBCA4\uD2B8 payload\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
555
|
+
}
|
|
556
|
+
const clean = sanitizeEvent(type, payload);
|
|
557
|
+
current = applyEvent(current, type, clean);
|
|
558
|
+
const frame = iframe.contentWindow;
|
|
559
|
+
if (frame) post(frame, createEnvelope("evt", type, createSession(), clean));
|
|
560
|
+
},
|
|
561
|
+
dispose() {
|
|
562
|
+
if (disposed) return;
|
|
563
|
+
disposed = true;
|
|
564
|
+
win.removeEventListener("message", onMessage);
|
|
565
|
+
if (resizeHandle !== void 0) cancelRaf(resizeHandle);
|
|
566
|
+
resizeHandle = void 0;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
export {
|
|
571
|
+
ALLOWED_METHODS,
|
|
572
|
+
ALLOWED_REQUEST_HEADERS,
|
|
573
|
+
BRIDGE_SURFACES,
|
|
574
|
+
DEFAULT_GATEWAY_DENY_SEGMENTS,
|
|
575
|
+
DEFAULT_POLICY,
|
|
576
|
+
HOST_ONLY_HEADERS,
|
|
577
|
+
IFRAME_ATTRIBUTES,
|
|
578
|
+
MAX_DECODE_PASSES,
|
|
579
|
+
MAX_PARTNER_ORIGINS,
|
|
580
|
+
checkPath,
|
|
581
|
+
classifyDestination,
|
|
582
|
+
createBridgeHost,
|
|
583
|
+
createNonce,
|
|
584
|
+
createSession,
|
|
585
|
+
createSrcdoc,
|
|
586
|
+
isAllowedGatewayPath,
|
|
587
|
+
isBridgeSurface,
|
|
588
|
+
isValidSession,
|
|
589
|
+
normalizePartnerOrigins,
|
|
590
|
+
normalizePinnedHeaders,
|
|
591
|
+
prepareFetch,
|
|
592
|
+
resolveDestination,
|
|
593
|
+
resolvePolicy
|
|
594
|
+
};
|