@bstage-sdk/bridge 0.0.0-stage → 0.9.1
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
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bridgeRuntime,
|
|
3
|
+
createBridgeFetch
|
|
4
|
+
} from "./chunk-KQMJY4ZH.js";
|
|
5
|
+
|
|
6
|
+
// src/protocol/envelope.ts
|
|
7
|
+
var BRIDGE_VERSION = 1;
|
|
8
|
+
var BRIDGE_NAMESPACE = "bstage-bridge";
|
|
9
|
+
var MAX_ENVELOPE_ID_LENGTH = 64;
|
|
10
|
+
var MAX_ENVELOPE_TYPE_LENGTH = 32;
|
|
11
|
+
var ENVELOPE_KINDS = ["req", "res", "evt"];
|
|
12
|
+
function isNonEmptyString(value, max) {
|
|
13
|
+
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
14
|
+
}
|
|
15
|
+
function isBridgeEnvelope(value) {
|
|
16
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17
|
+
const env = value;
|
|
18
|
+
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;
|
|
19
|
+
}
|
|
20
|
+
function createEnvelope(kind, type, id, payload) {
|
|
21
|
+
return { v: BRIDGE_VERSION, ns: BRIDGE_NAMESPACE, id, kind, type, payload };
|
|
22
|
+
}
|
|
23
|
+
function hasSession(payload, session) {
|
|
24
|
+
return typeof payload === "object" && payload !== null && payload.session === session;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/protocol/errors.ts
|
|
28
|
+
var BRIDGE_ERROR_CODES = [
|
|
29
|
+
"FORBIDDEN_DESTINATION",
|
|
30
|
+
"FORBIDDEN_METHOD",
|
|
31
|
+
"BODY_TOO_LARGE",
|
|
32
|
+
"RATE_LIMITED",
|
|
33
|
+
"BAD_REQUEST",
|
|
34
|
+
"TIMEOUT",
|
|
35
|
+
"NETWORK_ERROR",
|
|
36
|
+
"UNSUPPORTED"
|
|
37
|
+
];
|
|
38
|
+
var BridgeError = class extends Error {
|
|
39
|
+
constructor(code, message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "BridgeError";
|
|
42
|
+
this.code = code;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isBridgeErrorCode(value) {
|
|
46
|
+
return BRIDGE_ERROR_CODES.includes(value);
|
|
47
|
+
}
|
|
48
|
+
function isBridgeErrorLike(value) {
|
|
49
|
+
return value instanceof Error && isBridgeErrorCode(value.code);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/protocol/messages.ts
|
|
53
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
54
|
+
var REQUEST_TYPES = [
|
|
55
|
+
"fetch",
|
|
56
|
+
"navigate",
|
|
57
|
+
"back",
|
|
58
|
+
"openExternal",
|
|
59
|
+
"toast",
|
|
60
|
+
"resize",
|
|
61
|
+
"track",
|
|
62
|
+
"storage",
|
|
63
|
+
"log"
|
|
64
|
+
];
|
|
65
|
+
var HOST_EVENT_TYPES = ["slot.context", "locale", "member", "theme", "visibility"];
|
|
66
|
+
var READY_EVENT = "ready";
|
|
67
|
+
var INIT_RESPONSE = "init";
|
|
68
|
+
var BRIDGE_SURFACES = ["page", "slot"];
|
|
69
|
+
function isBridgeSurface(value) {
|
|
70
|
+
return BRIDGE_SURFACES.includes(value);
|
|
71
|
+
}
|
|
72
|
+
var IFRAME_ATTRIBUTES = {
|
|
73
|
+
sandbox: "allow-scripts",
|
|
74
|
+
allow: "autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write",
|
|
75
|
+
referrerpolicy: "no-referrer"
|
|
76
|
+
};
|
|
77
|
+
function isRequestType(value) {
|
|
78
|
+
return REQUEST_TYPES.includes(value);
|
|
79
|
+
}
|
|
80
|
+
function isHostEventType(value) {
|
|
81
|
+
return HOST_EVENT_TYPES.includes(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/protocol/session.ts
|
|
85
|
+
var SESSION_RE = /^[0-9a-f]{32}$/;
|
|
86
|
+
var NONCE_RE = /^[A-Za-z0-9+/_-]{22,}={0,2}$/;
|
|
87
|
+
function randomBytes(length) {
|
|
88
|
+
const bytes = new Uint8Array(length);
|
|
89
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
90
|
+
return bytes;
|
|
91
|
+
}
|
|
92
|
+
function createSession() {
|
|
93
|
+
return Array.from(randomBytes(16), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
94
|
+
}
|
|
95
|
+
function isValidSession(value) {
|
|
96
|
+
return typeof value === "string" && SESSION_RE.test(value);
|
|
97
|
+
}
|
|
98
|
+
function createNonce() {
|
|
99
|
+
const binary = String.fromCharCode(...randomBytes(16));
|
|
100
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
101
|
+
}
|
|
102
|
+
function isValidNonce(value) {
|
|
103
|
+
return typeof value === "string" && NONCE_RE.test(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/host/pathSafety.ts
|
|
107
|
+
var MAX_DECODE_PASSES = 3;
|
|
108
|
+
var REJECT = Object.freeze({ ok: false, forms: Object.freeze([]) });
|
|
109
|
+
function decodeStages(pathname) {
|
|
110
|
+
const forms = [pathname];
|
|
111
|
+
let current = pathname;
|
|
112
|
+
for (let pass = 0; pass <= MAX_DECODE_PASSES; pass += 1) {
|
|
113
|
+
let next;
|
|
114
|
+
try {
|
|
115
|
+
next = decodeURIComponent(current);
|
|
116
|
+
} catch {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
if (next === current) return forms;
|
|
120
|
+
if (pass === MAX_DECODE_PASSES) return void 0;
|
|
121
|
+
forms.push(next);
|
|
122
|
+
current = next;
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
var UNSAFE_CHARS_RE = /[\\;\u0000-\u001f\u007f]/;
|
|
127
|
+
function hasDotSegment(form) {
|
|
128
|
+
return form.split("/").some((segment) => segment === "." || segment === "..");
|
|
129
|
+
}
|
|
130
|
+
function checkPath(pathname, options = {}) {
|
|
131
|
+
const forms = decodeStages(pathname);
|
|
132
|
+
if (!forms) return REJECT;
|
|
133
|
+
const bad = forms.some(
|
|
134
|
+
(form) => UNSAFE_CHARS_RE.test(form) || hasDotSegment(form) || options.forbidDoubleSlash === true && form.includes("//")
|
|
135
|
+
);
|
|
136
|
+
return bad ? REJECT : { ok: true, forms: Object.freeze(forms) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/host/policy.ts
|
|
140
|
+
var DEFAULT_GATEWAY_DENY_SEGMENTS = Object.freeze([
|
|
141
|
+
"auth",
|
|
142
|
+
"logout",
|
|
143
|
+
"mfa",
|
|
144
|
+
"token",
|
|
145
|
+
"oauth",
|
|
146
|
+
"sso",
|
|
147
|
+
"debug",
|
|
148
|
+
"debugger",
|
|
149
|
+
"xdebug",
|
|
150
|
+
"withdraw"
|
|
151
|
+
]);
|
|
152
|
+
var ALWAYS_DENIED_SEGMENT_SUBSTRINGS = Object.freeze(["token"]);
|
|
153
|
+
var DEFAULT_POLICY = Object.freeze({
|
|
154
|
+
gatewayPrefixes: Object.freeze(["/gw/"]),
|
|
155
|
+
gatewayDenyPrefixes: Object.freeze(["/gw/account/v1/auth/", "/gw/account/v1/users/logout"]),
|
|
156
|
+
gatewayDenySegments: DEFAULT_GATEWAY_DENY_SEGMENTS,
|
|
157
|
+
allowExternal: false,
|
|
158
|
+
maxBodyBytes: 1024 * 1024,
|
|
159
|
+
maxRequestsPerMinute: 300,
|
|
160
|
+
maxResponseBytes: 10 * 1024 * 1024
|
|
161
|
+
});
|
|
162
|
+
var ALLOWED_REQUEST_HEADERS = Object.freeze([
|
|
163
|
+
"content-type",
|
|
164
|
+
"accept",
|
|
165
|
+
"accept-language",
|
|
166
|
+
// BstageClient가 보내는 앱 자격 헤더(파트너 콘솔 발급값, 브라우저 노출 전제 — 회원 토큰이 아니다).
|
|
167
|
+
// 호스트가 `pinnedHeaders`로 값을 주면 그 값이 이긴다.
|
|
168
|
+
"x-bstage-app-id",
|
|
169
|
+
"x-bstage-app-key",
|
|
170
|
+
// BstageClient가 모든 요청에 싣는 core 버전(정보용, 비밀 아님).
|
|
171
|
+
"x-bmf-sdk-version"
|
|
172
|
+
]);
|
|
173
|
+
var HOST_ONLY_HEADERS = Object.freeze([
|
|
174
|
+
"x-bstage-tenant-id",
|
|
175
|
+
"x-bstage-org-id",
|
|
176
|
+
"x-bstage-enterprise-id"
|
|
177
|
+
]);
|
|
178
|
+
var ALLOWED_METHODS = Object.freeze([
|
|
179
|
+
"GET",
|
|
180
|
+
"HEAD",
|
|
181
|
+
"POST",
|
|
182
|
+
"PUT",
|
|
183
|
+
"PATCH",
|
|
184
|
+
"DELETE"
|
|
185
|
+
]);
|
|
186
|
+
var MAX_PARTNER_ORIGINS = 10;
|
|
187
|
+
var MAX_URL_LENGTH = 8192;
|
|
188
|
+
function isPositiveInteger(value) {
|
|
189
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
190
|
+
}
|
|
191
|
+
function resolvePolicy(policy) {
|
|
192
|
+
const merged = { ...DEFAULT_POLICY, ...policy ?? {} };
|
|
193
|
+
if (!Array.isArray(merged.gatewayPrefixes) || merged.gatewayPrefixes.length === 0 || !merged.gatewayPrefixes.every((p) => typeof p === "string" && /^\/[^/]/.test(p))) {
|
|
194
|
+
throw new TypeError('[bstage-bridge] gatewayPrefixes\uB294 "/"\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
|
|
195
|
+
}
|
|
196
|
+
if (!Array.isArray(merged.gatewayDenyPrefixes) || !merged.gatewayDenyPrefixes.every((p) => typeof p === "string" && /^\/[^/]/.test(p))) {
|
|
197
|
+
throw new TypeError(
|
|
198
|
+
'[bstage-bridge] gatewayDenyPrefixes\uB294 "/"\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(merged.gatewayDenySegments) || !merged.gatewayDenySegments.every(
|
|
202
|
+
(x) => typeof x === "string" && x.length > 0 && !x.includes("/")
|
|
203
|
+
)) {
|
|
204
|
+
throw new TypeError('[bstage-bridge] gatewayDenySegments\uB294 "/" \uC5C6\uB294 \uC138\uADF8\uBA3C\uD2B8 \uBAA9\uB85D\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4');
|
|
205
|
+
}
|
|
206
|
+
if (typeof merged.allowExternal !== "boolean") {
|
|
207
|
+
throw new TypeError("[bstage-bridge] allowExternal\uC740 \uBD88\uB9AC\uC5B8\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
208
|
+
}
|
|
209
|
+
if (!isPositiveInteger(merged.maxBodyBytes) || !isPositiveInteger(merged.maxRequestsPerMinute) || !isPositiveInteger(merged.maxResponseBytes)) {
|
|
210
|
+
throw new TypeError(
|
|
211
|
+
"[bstage-bridge] maxBodyBytes\xB7maxRequestsPerMinute\xB7maxResponseBytes\uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
...merged,
|
|
216
|
+
gatewayPrefixes: Object.freeze([...merged.gatewayPrefixes]),
|
|
217
|
+
gatewayDenyPrefixes: Object.freeze([...merged.gatewayDenyPrefixes]),
|
|
218
|
+
gatewayDenySegments: Object.freeze(merged.gatewayDenySegments.map((x) => x.toLowerCase()))
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
var HOSTNAME_RE = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/;
|
|
222
|
+
function normalizeExactOrigin(value, protocols) {
|
|
223
|
+
if (typeof value !== "string") return void 0;
|
|
224
|
+
try {
|
|
225
|
+
const url = new URL(value);
|
|
226
|
+
if (!protocols.includes(url.protocol) || !HOSTNAME_RE.test(url.hostname)) return void 0;
|
|
227
|
+
return url.origin === value ? value : void 0;
|
|
228
|
+
} catch {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function normalizePartnerOrigins(origins, excludeOrigin) {
|
|
233
|
+
if (!Array.isArray(origins)) return Object.freeze([]);
|
|
234
|
+
const valid = origins.map((o) => normalizeExactOrigin(o, ["https:"])).filter((o) => o !== void 0 && o !== excludeOrigin);
|
|
235
|
+
return Object.freeze([...new Set(valid)].slice(0, MAX_PARTNER_ORIGINS));
|
|
236
|
+
}
|
|
237
|
+
function matchesPrefix(pathname, prefix) {
|
|
238
|
+
const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
239
|
+
return pathname === bare || pathname.startsWith(`${bare}/`);
|
|
240
|
+
}
|
|
241
|
+
function denyKey(form) {
|
|
242
|
+
return form.replace(/\/{2,}/g, "/").toLowerCase();
|
|
243
|
+
}
|
|
244
|
+
function isAllowedGatewayPath(pathname, policy) {
|
|
245
|
+
const check = checkPath(pathname);
|
|
246
|
+
if (!check.ok) return false;
|
|
247
|
+
if (!policy.gatewayPrefixes.some((p) => matchesPrefix(pathname, p))) return false;
|
|
248
|
+
const deny = policy.gatewayDenyPrefixes.map((p) => p.toLowerCase());
|
|
249
|
+
const denySegments = policy.gatewayDenySegments;
|
|
250
|
+
return !check.forms.some((form) => {
|
|
251
|
+
const key = denyKey(form);
|
|
252
|
+
return deny.some((p) => matchesPrefix(key, p)) || key.split("/").some(
|
|
253
|
+
(segment) => denySegments.includes(segment) || ALWAYS_DENIED_SEGMENT_SUBSTRINGS.some((part) => segment.includes(part))
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
var LOCAL_HOST_NAMES = Object.freeze([
|
|
258
|
+
"localhost",
|
|
259
|
+
"local",
|
|
260
|
+
"internal",
|
|
261
|
+
"home.arpa",
|
|
262
|
+
"test",
|
|
263
|
+
"invalid",
|
|
264
|
+
"example",
|
|
265
|
+
"lan",
|
|
266
|
+
"corp",
|
|
267
|
+
"intranet",
|
|
268
|
+
"localdomain"
|
|
269
|
+
]);
|
|
270
|
+
function isExternalHostAllowed(hostname, hostHostname) {
|
|
271
|
+
const name = hostname.replace(/\.+$/, "");
|
|
272
|
+
if (name === "" || name.startsWith("[") || /^[0-9.]+$/.test(name)) return false;
|
|
273
|
+
const labels = name.split(".");
|
|
274
|
+
if (labels.length < 2 || labels.some((label) => label === "")) return false;
|
|
275
|
+
if (LOCAL_HOST_NAMES.some((base) => name === base || name.endsWith(`.${base}`))) return false;
|
|
276
|
+
return name !== hostHostname.replace(/\.+$/, "");
|
|
277
|
+
}
|
|
278
|
+
function classifyDestination(rawUrl, ctx) {
|
|
279
|
+
if (typeof rawUrl !== "string" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {
|
|
280
|
+
throw new BridgeError("BAD_REQUEST", "invalid url");
|
|
281
|
+
}
|
|
282
|
+
let url;
|
|
283
|
+
try {
|
|
284
|
+
url = new URL(rawUrl, ctx.hostOrigin);
|
|
285
|
+
} catch {
|
|
286
|
+
throw new BridgeError("BAD_REQUEST", "invalid url");
|
|
287
|
+
}
|
|
288
|
+
if (url.username !== "" || url.password !== "") {
|
|
289
|
+
throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
|
|
290
|
+
}
|
|
291
|
+
const host = new URL(ctx.hostOrigin);
|
|
292
|
+
if (url.origin === host.origin) {
|
|
293
|
+
if (url.protocol === host.protocol && isAllowedGatewayPath(url.pathname, ctx.policy)) {
|
|
294
|
+
return { url: url.href, kind: "gateway" };
|
|
295
|
+
}
|
|
296
|
+
throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
|
|
297
|
+
}
|
|
298
|
+
if (url.protocol !== "https:") {
|
|
299
|
+
throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
|
|
300
|
+
}
|
|
301
|
+
if (ctx.partnerOrigins.includes(url.origin)) return { url: url.href, kind: "partner" };
|
|
302
|
+
if (ctx.policy.allowExternal && isExternalHostAllowed(url.hostname, host.hostname)) {
|
|
303
|
+
return { url: url.href, kind: "external" };
|
|
304
|
+
}
|
|
305
|
+
throw new BridgeError("FORBIDDEN_DESTINATION", "destination not allowed");
|
|
306
|
+
}
|
|
307
|
+
function resolveDestination(rawUrl, ctx) {
|
|
308
|
+
return classifyDestination(rawUrl, ctx).url;
|
|
309
|
+
}
|
|
310
|
+
function resolveMethod(method) {
|
|
311
|
+
if (method === void 0) return "GET";
|
|
312
|
+
if (typeof method !== "string") throw new BridgeError("BAD_REQUEST", "invalid method");
|
|
313
|
+
const upper = method.toUpperCase();
|
|
314
|
+
if (!ALLOWED_METHODS.includes(upper)) {
|
|
315
|
+
throw new BridgeError("FORBIDDEN_METHOD", "method not allowed");
|
|
316
|
+
}
|
|
317
|
+
return upper;
|
|
318
|
+
}
|
|
319
|
+
var UNSAFE_HEADER_VALUE_RE = /[\r\n\0]/;
|
|
320
|
+
function normalizePinnedHeaders(pinned) {
|
|
321
|
+
if (pinned === void 0) return Object.freeze({});
|
|
322
|
+
if (typeof pinned !== "object" || pinned === null || Array.isArray(pinned)) {
|
|
323
|
+
throw new TypeError("[bstage-bridge] pinnedHeaders\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
324
|
+
}
|
|
325
|
+
const entries = Object.entries(pinned).map(([k, v]) => {
|
|
326
|
+
const key = k.toLowerCase();
|
|
327
|
+
if (!ALLOWED_REQUEST_HEADERS.includes(key) && !HOST_ONLY_HEADERS.includes(key)) {
|
|
328
|
+
throw new TypeError(`[bstage-bridge] pinnedHeaders\uC5D0 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD5E4\uB354: ${k}`);
|
|
329
|
+
}
|
|
330
|
+
if (typeof v !== "string" || UNSAFE_HEADER_VALUE_RE.test(v)) {
|
|
331
|
+
throw new TypeError(`[bstage-bridge] pinnedHeaders \uAC12\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4: ${k}`);
|
|
332
|
+
}
|
|
333
|
+
return [key, v];
|
|
334
|
+
});
|
|
335
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
336
|
+
}
|
|
337
|
+
function mergeHeaders(frameHeaders, pinned) {
|
|
338
|
+
return Object.freeze({ ...filterHeaders(frameHeaders), ...pinned });
|
|
339
|
+
}
|
|
340
|
+
function filterHeaders(headers) {
|
|
341
|
+
if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return {};
|
|
342
|
+
const entries = Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]).filter(
|
|
343
|
+
(entry) => ALLOWED_REQUEST_HEADERS.includes(entry[0]) && typeof entry[1] === "string" && !/[\r\n\0]/.test(entry[1])
|
|
344
|
+
);
|
|
345
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
346
|
+
}
|
|
347
|
+
function exceedsBytes(body, max) {
|
|
348
|
+
if (body.length > max) return true;
|
|
349
|
+
if (body.length * 3 <= max) return false;
|
|
350
|
+
return new TextEncoder().encode(body).length > max;
|
|
351
|
+
}
|
|
352
|
+
function resolveBody(body, method, maxBodyBytes) {
|
|
353
|
+
if (body === void 0 || body === null) return void 0;
|
|
354
|
+
if (typeof body !== "string") throw new BridgeError("BAD_REQUEST", "body must be a string");
|
|
355
|
+
if (method === "GET" || method === "HEAD") {
|
|
356
|
+
throw new BridgeError("BAD_REQUEST", "GET/HEAD cannot have a body");
|
|
357
|
+
}
|
|
358
|
+
if (exceedsBytes(body, maxBodyBytes)) throw new BridgeError("BODY_TOO_LARGE", "body too large");
|
|
359
|
+
return body;
|
|
360
|
+
}
|
|
361
|
+
function prepareFetch(payload, ctx) {
|
|
362
|
+
if (typeof payload !== "object" || payload === null) {
|
|
363
|
+
throw new BridgeError("BAD_REQUEST", "invalid payload");
|
|
364
|
+
}
|
|
365
|
+
const req = payload;
|
|
366
|
+
const method = resolveMethod(req.method);
|
|
367
|
+
const { url, kind } = classifyDestination(req.url, ctx);
|
|
368
|
+
const body = resolveBody(req.body, method, ctx.policy.maxBodyBytes);
|
|
369
|
+
const headers = kind === "gateway" ? mergeHeaders(req.headers, ctx.pinnedHeaders ?? {}) : filterHeaders(req.headers);
|
|
370
|
+
return {
|
|
371
|
+
url,
|
|
372
|
+
kind,
|
|
373
|
+
init: {
|
|
374
|
+
method,
|
|
375
|
+
headers,
|
|
376
|
+
...body !== void 0 ? { body } : {},
|
|
377
|
+
credentials: kind === "gateway" ? "same-origin" : "omit",
|
|
378
|
+
...kind === "gateway" ? {} : { referrerPolicy: "no-referrer" },
|
|
379
|
+
// 브리지 프로토콜 인자에 더한 방어선: redirect를 따라가면 판정한 URL과 실제 목적지가 달라진다
|
|
380
|
+
// (파트너 origin이 같은 origin의 /gw 밖 경로로 돌려보내면 쿠키가 실린 요청이 된다).
|
|
381
|
+
redirect: "error"
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/srcdoc/escape.ts
|
|
387
|
+
var HTML_ESCAPES = {
|
|
388
|
+
"&": "&",
|
|
389
|
+
"<": "<",
|
|
390
|
+
">": ">",
|
|
391
|
+
'"': """,
|
|
392
|
+
"'": "'"
|
|
393
|
+
};
|
|
394
|
+
function escapeHtml(value) {
|
|
395
|
+
return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
|
|
396
|
+
}
|
|
397
|
+
function toScriptLiteral(value) {
|
|
398
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/srcdoc/createSrcdoc.ts
|
|
402
|
+
var MAX_MEDIA_ORIGINS = 20;
|
|
403
|
+
var MAX_ELEMENT_NAME_LENGTH = 100;
|
|
404
|
+
var ELEMENT_NAME_RE = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
|
|
405
|
+
function isValidElementName(value) {
|
|
406
|
+
return typeof value === "string" && value.length <= MAX_ELEMENT_NAME_LENGTH && ELEMENT_NAME_RE.test(value);
|
|
407
|
+
}
|
|
408
|
+
function assertOrigin(label, value) {
|
|
409
|
+
const origin = normalizeExactOrigin(value, ["https:", "http:"]);
|
|
410
|
+
if (!origin) throw new TypeError(`[bstage-bridge] ${label}\uB294 \uC815\uD655\uD55C origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
|
|
411
|
+
return origin;
|
|
412
|
+
}
|
|
413
|
+
function assertEntryUrl(entryUrl, assetOrigin) {
|
|
414
|
+
let parsed;
|
|
415
|
+
try {
|
|
416
|
+
parsed = new URL(entryUrl);
|
|
417
|
+
} catch {
|
|
418
|
+
throw new TypeError("[bstage-bridge] entryUrl\uC774 URL\uC774 \uC544\uB2D9\uB2C8\uB2E4");
|
|
419
|
+
}
|
|
420
|
+
if (parsed.origin !== assetOrigin || parsed.username || parsed.password) {
|
|
421
|
+
throw new TypeError("[bstage-bridge] entryUrl\uC740 assetOrigin\uC5D0 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
422
|
+
}
|
|
423
|
+
return parsed.href;
|
|
424
|
+
}
|
|
425
|
+
function normalizeMediaOrigins(value) {
|
|
426
|
+
if (value === void 0) return Object.freeze([]);
|
|
427
|
+
if (!Array.isArray(value)) {
|
|
428
|
+
throw new TypeError("[bstage-bridge] mediaOrigins\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
429
|
+
}
|
|
430
|
+
if (value.length > MAX_MEDIA_ORIGINS) {
|
|
431
|
+
throw new TypeError(`[bstage-bridge] mediaOrigins\uB294 \uCD5C\uB300 ${MAX_MEDIA_ORIGINS}\uAC1C\uC785\uB2C8\uB2E4`);
|
|
432
|
+
}
|
|
433
|
+
const origins = value.map((o) => {
|
|
434
|
+
const origin = normalizeExactOrigin(o, ["https:"]);
|
|
435
|
+
if (!origin) {
|
|
436
|
+
throw new TypeError("[bstage-bridge] mediaOrigins \uD56D\uBAA9\uC740 \uC815\uD655\uD55C https origin\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
437
|
+
}
|
|
438
|
+
return origin;
|
|
439
|
+
});
|
|
440
|
+
return Object.freeze([...new Set(origins)]);
|
|
441
|
+
}
|
|
442
|
+
function buildFrameCsp(nonce, assetOrigin, mediaOrigins = []) {
|
|
443
|
+
const media = [assetOrigin, ...normalizeMediaOrigins(mediaOrigins)].join(" ");
|
|
444
|
+
return [
|
|
445
|
+
"default-src 'none'",
|
|
446
|
+
`script-src 'nonce-${nonce}' ${assetOrigin}`,
|
|
447
|
+
`style-src 'unsafe-inline' ${assetOrigin}`,
|
|
448
|
+
`img-src ${media} data: blob:`,
|
|
449
|
+
`font-src ${assetOrigin} data:`,
|
|
450
|
+
`media-src ${media} blob:`,
|
|
451
|
+
"connect-src 'none'",
|
|
452
|
+
"frame-src 'none'",
|
|
453
|
+
"form-action 'none'",
|
|
454
|
+
"base-uri 'none'"
|
|
455
|
+
].join("; ");
|
|
456
|
+
}
|
|
457
|
+
function bootstrapScript(o) {
|
|
458
|
+
return [
|
|
459
|
+
"(function () {",
|
|
460
|
+
'"use strict";',
|
|
461
|
+
`var SESSION = ${toScriptLiteral(o.session)};`,
|
|
462
|
+
`var HOST_ORIGIN = ${toScriptLiteral(o.hostOrigin)};`,
|
|
463
|
+
`var ENTRY_URL = ${toScriptLiteral(o.entryUrl)};`,
|
|
464
|
+
`var ELEMENT_NAME = ${toScriptLiteral(o.elementName)};`,
|
|
465
|
+
`var bridgeRuntime = ${bridgeRuntime.toString()};`,
|
|
466
|
+
`var createBridgeFetch = ${createBridgeFetch.toString()};`,
|
|
467
|
+
"var client = bridgeRuntime(globalThis, createBridgeFetch, { session: SESSION, hostOrigin: HOST_ORIGIN });",
|
|
468
|
+
"var lastHeight = -1;",
|
|
469
|
+
"var scheduled = false;",
|
|
470
|
+
"function reportHeight() {",
|
|
471
|
+
" scheduled = false;",
|
|
472
|
+
" var h = Math.ceil(document.body.getBoundingClientRect().height);",
|
|
473
|
+
" if (h === lastHeight) return;",
|
|
474
|
+
" lastHeight = h;",
|
|
475
|
+
" client.request('resize', { height: h }).catch(function () {});",
|
|
476
|
+
"}",
|
|
477
|
+
"function scheduleHeight() {",
|
|
478
|
+
" if (scheduled) return;",
|
|
479
|
+
" scheduled = true;",
|
|
480
|
+
" requestAnimationFrame(reportHeight);",
|
|
481
|
+
"}",
|
|
482
|
+
"client.ready.then(function () {",
|
|
483
|
+
" var script = document.createElement('script');",
|
|
484
|
+
" script.src = ENTRY_URL;",
|
|
485
|
+
" script.onerror = function () {",
|
|
486
|
+
" client.request('log', { level: 'error', message: 'template entry failed to load' }).catch(function () {});",
|
|
487
|
+
" };",
|
|
488
|
+
" document.head.appendChild(script);",
|
|
489
|
+
" customElements.whenDefined(ELEMENT_NAME).then(function () {",
|
|
490
|
+
" document.body.appendChild(document.createElement(ELEMENT_NAME));",
|
|
491
|
+
" if (typeof ResizeObserver === 'function') {",
|
|
492
|
+
" new ResizeObserver(scheduleHeight).observe(document.body);",
|
|
493
|
+
" }",
|
|
494
|
+
" scheduleHeight();",
|
|
495
|
+
" });",
|
|
496
|
+
"}, function () {});",
|
|
497
|
+
"})();"
|
|
498
|
+
].join("\n");
|
|
499
|
+
}
|
|
500
|
+
function createSrcdoc(options) {
|
|
501
|
+
const { nonce, session, title, elementName } = options;
|
|
502
|
+
if (!isValidNonce(nonce)) throw new TypeError("[bstage-bridge] nonce \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4");
|
|
503
|
+
if (!isValidSession(session)) throw new TypeError("[bstage-bridge] session \uD615\uC2DD\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4");
|
|
504
|
+
const hostOrigin = assertOrigin("hostOrigin", options.hostOrigin);
|
|
505
|
+
const assetOrigin = assertOrigin("assetOrigin", options.assetOrigin);
|
|
506
|
+
const entryUrl = assertEntryUrl(options.entryUrl, assetOrigin);
|
|
507
|
+
if (!isValidElementName(elementName)) {
|
|
508
|
+
throw new TypeError("[bstage-bridge] elementName\uC774 custom element \uC774\uB984\uC774 \uC544\uB2D9\uB2C8\uB2E4");
|
|
509
|
+
}
|
|
510
|
+
if (typeof title !== "string") throw new TypeError("[bstage-bridge] title\uC740 \uBB38\uC790\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4");
|
|
511
|
+
const mediaOrigins = normalizeMediaOrigins(options.mediaOrigins);
|
|
512
|
+
const csp = buildFrameCsp(nonce, assetOrigin, mediaOrigins);
|
|
513
|
+
const script = bootstrapScript({ session, hostOrigin, entryUrl, elementName });
|
|
514
|
+
if (/<\/script|<!--/i.test(script)) {
|
|
515
|
+
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");
|
|
516
|
+
}
|
|
517
|
+
return [
|
|
518
|
+
"<!doctype html>",
|
|
519
|
+
"<html>",
|
|
520
|
+
"<head>",
|
|
521
|
+
'<meta charset="utf-8">',
|
|
522
|
+
`<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">`,
|
|
523
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
524
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
525
|
+
// body 높이를 콘텐츠 높이로 잰다 — documentElement.scrollHeight는 뷰포트 이상이라 줄어들지 못한다.
|
|
526
|
+
// flow-root는 자식 margin이 body 밖으로 접혀 나가 높이에서 빠지는 것을 막는다.
|
|
527
|
+
"<style>html,body{margin:0;padding:0;background:transparent;height:auto}body{display:flow-root}</style>",
|
|
528
|
+
`<script nonce="${escapeHtml(nonce)}">
|
|
529
|
+
${script}
|
|
530
|
+
</script>`,
|
|
531
|
+
"</head>",
|
|
532
|
+
"<body></body>",
|
|
533
|
+
"</html>"
|
|
534
|
+
].join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export {
|
|
538
|
+
BRIDGE_VERSION,
|
|
539
|
+
BRIDGE_NAMESPACE,
|
|
540
|
+
MAX_ENVELOPE_ID_LENGTH,
|
|
541
|
+
MAX_ENVELOPE_TYPE_LENGTH,
|
|
542
|
+
isBridgeEnvelope,
|
|
543
|
+
createEnvelope,
|
|
544
|
+
hasSession,
|
|
545
|
+
BRIDGE_ERROR_CODES,
|
|
546
|
+
BridgeError,
|
|
547
|
+
isBridgeErrorCode,
|
|
548
|
+
isBridgeErrorLike,
|
|
549
|
+
REQUEST_TIMEOUT_MS,
|
|
550
|
+
REQUEST_TYPES,
|
|
551
|
+
HOST_EVENT_TYPES,
|
|
552
|
+
READY_EVENT,
|
|
553
|
+
INIT_RESPONSE,
|
|
554
|
+
BRIDGE_SURFACES,
|
|
555
|
+
isBridgeSurface,
|
|
556
|
+
IFRAME_ATTRIBUTES,
|
|
557
|
+
isRequestType,
|
|
558
|
+
isHostEventType,
|
|
559
|
+
createSession,
|
|
560
|
+
isValidSession,
|
|
561
|
+
createNonce,
|
|
562
|
+
isValidNonce,
|
|
563
|
+
MAX_DECODE_PASSES,
|
|
564
|
+
checkPath,
|
|
565
|
+
DEFAULT_GATEWAY_DENY_SEGMENTS,
|
|
566
|
+
DEFAULT_POLICY,
|
|
567
|
+
ALLOWED_REQUEST_HEADERS,
|
|
568
|
+
HOST_ONLY_HEADERS,
|
|
569
|
+
ALLOWED_METHODS,
|
|
570
|
+
MAX_PARTNER_ORIGINS,
|
|
571
|
+
resolvePolicy,
|
|
572
|
+
normalizeExactOrigin,
|
|
573
|
+
normalizePartnerOrigins,
|
|
574
|
+
isAllowedGatewayPath,
|
|
575
|
+
classifyDestination,
|
|
576
|
+
resolveDestination,
|
|
577
|
+
normalizePinnedHeaders,
|
|
578
|
+
prepareFetch,
|
|
579
|
+
MAX_MEDIA_ORIGINS,
|
|
580
|
+
MAX_ELEMENT_NAME_LENGTH,
|
|
581
|
+
buildFrameCsp,
|
|
582
|
+
createSrcdoc
|
|
583
|
+
};
|