@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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/frame/createBridgeFetch.ts
|
|
2
|
+
function createBridgeFetch(client) {
|
|
3
|
+
const NULL_BODY_STATUS = [204, 205, 304];
|
|
4
|
+
function toHeaderRecord(h) {
|
|
5
|
+
const out = {};
|
|
6
|
+
if (!h) return out;
|
|
7
|
+
new Headers(h).forEach(function(value, key) {
|
|
8
|
+
out[key] = value;
|
|
9
|
+
});
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
function abortError(signal) {
|
|
13
|
+
if (signal.reason !== void 0) return signal.reason;
|
|
14
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
15
|
+
}
|
|
16
|
+
function bridgeFetch(input, init) {
|
|
17
|
+
return new Promise(function(resolve, reject) {
|
|
18
|
+
const opts = init || {};
|
|
19
|
+
const isRequest = typeof input === "object" && input !== null && typeof input.method === "string";
|
|
20
|
+
const url = isRequest ? input.url : String(input);
|
|
21
|
+
const method = String(opts.method || (isRequest ? input.method : "GET"));
|
|
22
|
+
const headers = toHeaderRecord(
|
|
23
|
+
opts.headers || (isRequest ? input.headers : null)
|
|
24
|
+
);
|
|
25
|
+
let body = null;
|
|
26
|
+
if (isRequest && input.body !== null && opts.body == null) {
|
|
27
|
+
reject(new TypeError("bridge fetch: pass the body via init.body"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const raw = opts.body;
|
|
31
|
+
if (raw === void 0 || raw === null) {
|
|
32
|
+
} else if (typeof raw === "string") {
|
|
33
|
+
body = raw;
|
|
34
|
+
} else if (typeof URLSearchParams !== "undefined" && raw instanceof URLSearchParams) {
|
|
35
|
+
body = raw.toString();
|
|
36
|
+
if (!headers["content-type"]) {
|
|
37
|
+
headers["content-type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
reject(new TypeError("bridge fetch: unsupported body type"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const signal = opts.signal;
|
|
44
|
+
if (signal && signal.aborted) {
|
|
45
|
+
reject(abortError(signal));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let settled = false;
|
|
49
|
+
function onAbort() {
|
|
50
|
+
if (settled || !signal) return;
|
|
51
|
+
settled = true;
|
|
52
|
+
reject(abortError(signal));
|
|
53
|
+
}
|
|
54
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
55
|
+
const payload = { url, method, headers, body };
|
|
56
|
+
client.request("fetch", payload).then(
|
|
57
|
+
function(res) {
|
|
58
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
try {
|
|
62
|
+
const nullBody = method.toUpperCase() === "HEAD" || NULL_BODY_STATUS.indexOf(res.status) >= 0;
|
|
63
|
+
const responseHeaders = {};
|
|
64
|
+
if (res.headers && typeof res.headers["content-type"] === "string") {
|
|
65
|
+
responseHeaders["content-type"] = res.headers["content-type"];
|
|
66
|
+
}
|
|
67
|
+
resolve(
|
|
68
|
+
new Response(nullBody ? null : typeof res.bodyText === "string" ? res.bodyText : "", {
|
|
69
|
+
status: res.status,
|
|
70
|
+
statusText: res.statusText,
|
|
71
|
+
headers: responseHeaders
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
reject(new TypeError("bridge fetch: invalid response"));
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
function(err) {
|
|
79
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
const code = err && typeof err === "object" ? err.code : void 0;
|
|
83
|
+
const failure = new TypeError("bridge fetch failed: " + String(code || "NETWORK_ERROR"));
|
|
84
|
+
failure.code = code || "NETWORK_ERROR";
|
|
85
|
+
reject(failure);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return bridgeFetch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/frame/runtime.ts
|
|
94
|
+
function bridgeRuntime(g, makeFetch, config) {
|
|
95
|
+
const NS = "bstage-bridge";
|
|
96
|
+
const TIMEOUT_MS = 15e3;
|
|
97
|
+
const READY_INTERVAL_MS = 250;
|
|
98
|
+
const READY_FAST_ATTEMPTS = 40;
|
|
99
|
+
const READY_SLOW_INTERVAL_MS = 2e3;
|
|
100
|
+
const MAX_ID = 64;
|
|
101
|
+
const MAX_TYPE = 32;
|
|
102
|
+
if (g.__bstage_bridge__) return g.__bstage_bridge__;
|
|
103
|
+
const session = config.session;
|
|
104
|
+
const hostOrigin = config.hostOrigin;
|
|
105
|
+
const parent = g.parent;
|
|
106
|
+
const pending = /* @__PURE__ */ new Map();
|
|
107
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
108
|
+
let init;
|
|
109
|
+
let seq = 0;
|
|
110
|
+
function randomPrefix() {
|
|
111
|
+
const bytes = new Uint8Array(8);
|
|
112
|
+
if (g.crypto && typeof g.crypto.getRandomValues === "function") {
|
|
113
|
+
g.crypto.getRandomValues(bytes);
|
|
114
|
+
} else {
|
|
115
|
+
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
116
|
+
}
|
|
117
|
+
let out = "";
|
|
118
|
+
for (let i = 0; i < bytes.length; i += 1) out += (bytes[i] + 256).toString(16).slice(1);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
const idPrefix = randomPrefix();
|
|
122
|
+
function nextId() {
|
|
123
|
+
seq += 1;
|
|
124
|
+
return idPrefix + "-" + seq;
|
|
125
|
+
}
|
|
126
|
+
function codeError(code, message) {
|
|
127
|
+
const err = new Error(message);
|
|
128
|
+
err.name = "BridgeError";
|
|
129
|
+
err.code = code;
|
|
130
|
+
return err;
|
|
131
|
+
}
|
|
132
|
+
function post(kind, type, id, payload) {
|
|
133
|
+
if (!parent) return;
|
|
134
|
+
const body = Object.assign({}, payload, { session });
|
|
135
|
+
parent.postMessage({ v: 1, ns: NS, id, kind, type, payload: body }, hostOrigin);
|
|
136
|
+
}
|
|
137
|
+
function isEnvelope(m) {
|
|
138
|
+
if (typeof m !== "object" || m === null) return false;
|
|
139
|
+
const e = m;
|
|
140
|
+
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;
|
|
141
|
+
}
|
|
142
|
+
let resolveReady = function() {
|
|
143
|
+
};
|
|
144
|
+
const ready = new Promise(function(resolve) {
|
|
145
|
+
resolveReady = resolve;
|
|
146
|
+
});
|
|
147
|
+
function emit(type, payload) {
|
|
148
|
+
const fns = listeners.get(type);
|
|
149
|
+
if (!fns) return;
|
|
150
|
+
fns.slice().forEach(function(fn) {
|
|
151
|
+
try {
|
|
152
|
+
fn(payload);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function applyEvent(type, payload) {
|
|
158
|
+
if (!init || typeof payload !== "object" || payload === null) return;
|
|
159
|
+
const p = payload;
|
|
160
|
+
if (type === "locale" && typeof p.locale === "string") {
|
|
161
|
+
init = Object.assign({}, init, { locale: p.locale, translations: p.translations });
|
|
162
|
+
} else if (type === "member" && typeof p.signedIn === "boolean") {
|
|
163
|
+
init = Object.assign({}, init, { member: payload });
|
|
164
|
+
} else if (type === "theme" && typeof p.dark === "boolean") {
|
|
165
|
+
init = Object.assign({}, init, { theme: payload });
|
|
166
|
+
} else if (type === "slot.context" && typeof p.id === "string") {
|
|
167
|
+
init = Object.assign({}, init, { slot: payload });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function onMessage(ev) {
|
|
171
|
+
if (!parent || ev.source !== parent) return;
|
|
172
|
+
if (ev.origin !== hostOrigin) return;
|
|
173
|
+
const m = ev.data;
|
|
174
|
+
if (!isEnvelope(m)) return;
|
|
175
|
+
const result = m.payload;
|
|
176
|
+
if (m.kind === "res" && m.type === "init") {
|
|
177
|
+
if (!init && result && result.ok === true && result.data) {
|
|
178
|
+
init = result.data;
|
|
179
|
+
clearInterval(readyTimer);
|
|
180
|
+
resolveReady(init);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (m.kind === "res") {
|
|
185
|
+
const entry = pending.get(m.id);
|
|
186
|
+
if (!entry) return;
|
|
187
|
+
pending.delete(m.id);
|
|
188
|
+
clearTimeout(entry.timer);
|
|
189
|
+
if (result && result.ok === true) {
|
|
190
|
+
entry.resolve(result.data);
|
|
191
|
+
} else {
|
|
192
|
+
const error = result && result.error || {};
|
|
193
|
+
entry.reject(
|
|
194
|
+
codeError(
|
|
195
|
+
typeof error.code === "string" ? error.code : "BAD_REQUEST",
|
|
196
|
+
typeof error.message === "string" ? error.message : "bridge request failed"
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (m.kind === "evt") {
|
|
203
|
+
applyEvent(m.type, m.payload);
|
|
204
|
+
emit(m.type, m.payload);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
g.addEventListener("message", onMessage);
|
|
208
|
+
let readyAttempts = 0;
|
|
209
|
+
function sendReady() {
|
|
210
|
+
readyAttempts += 1;
|
|
211
|
+
if (readyAttempts === READY_FAST_ATTEMPTS) {
|
|
212
|
+
clearInterval(readyTimer);
|
|
213
|
+
readyTimer = setInterval(sendReady, READY_SLOW_INTERVAL_MS);
|
|
214
|
+
}
|
|
215
|
+
post("evt", "ready", nextId(), {});
|
|
216
|
+
}
|
|
217
|
+
let readyTimer = setInterval(sendReady, READY_INTERVAL_MS);
|
|
218
|
+
sendReady();
|
|
219
|
+
function request(type, payload) {
|
|
220
|
+
return new Promise(function(resolve, reject) {
|
|
221
|
+
const id = nextId();
|
|
222
|
+
const timer = setTimeout(function() {
|
|
223
|
+
pending.delete(id);
|
|
224
|
+
reject(codeError("TIMEOUT", "bridge request timed out: " + type));
|
|
225
|
+
}, TIMEOUT_MS);
|
|
226
|
+
pending.set(id, { resolve, reject, timer });
|
|
227
|
+
post("req", type, id, payload || {});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function on(type, fn) {
|
|
231
|
+
const current = listeners.get(type) || [];
|
|
232
|
+
listeners.set(type, current.concat([fn]));
|
|
233
|
+
return function() {
|
|
234
|
+
const now = listeners.get(type) || [];
|
|
235
|
+
listeners.set(
|
|
236
|
+
type,
|
|
237
|
+
now.filter(function(f) {
|
|
238
|
+
return f !== fn;
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const base = {
|
|
244
|
+
version: 1,
|
|
245
|
+
hostOrigin,
|
|
246
|
+
ready,
|
|
247
|
+
getInit: function() {
|
|
248
|
+
return init;
|
|
249
|
+
},
|
|
250
|
+
request,
|
|
251
|
+
on
|
|
252
|
+
};
|
|
253
|
+
const client = Object.freeze(Object.assign({}, base, { fetch: makeFetch(base) }));
|
|
254
|
+
Object.defineProperty(g, "__bstage_bridge__", {
|
|
255
|
+
value: client,
|
|
256
|
+
writable: false,
|
|
257
|
+
configurable: false,
|
|
258
|
+
enumerable: false
|
|
259
|
+
});
|
|
260
|
+
return client;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
bridgeRuntime,
|
|
265
|
+
createBridgeFetch
|
|
266
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** 프레임마다 새 세션(128비트 임의값)을 만든다. srcdoc 부트스트랩에만 심는다. */
|
|
2
|
+
declare function createSession(): string;
|
|
3
|
+
declare function isValidSession(value: unknown): value is string;
|
|
4
|
+
/** srcdoc 부트스트랩 스크립트용 CSP nonce(128비트, base64url)를 만든다. */
|
|
5
|
+
declare function createNonce(): string;
|
|
6
|
+
declare function isValidNonce(value: unknown): value is string;
|
|
7
|
+
|
|
8
|
+
interface CreateSrcdocOptions {
|
|
9
|
+
/** 부트스트랩 스크립트 CSP nonce(`createNonce()`). */
|
|
10
|
+
readonly nonce: string;
|
|
11
|
+
/** 프레임 세션(`createSession()`). */
|
|
12
|
+
readonly session: string;
|
|
13
|
+
/** 호스트 페이지 origin. 프레임 → 호스트 postMessage의 targetOrigin. */
|
|
14
|
+
readonly hostOrigin: string;
|
|
15
|
+
/** 템플릿 번들 CDN origin. CSP script·style·img·font·media 허용 대상. */
|
|
16
|
+
readonly assetOrigin: string;
|
|
17
|
+
/** 템플릿 엔트리 스크립트 URL. origin이 assetOrigin이어야 한다. */
|
|
18
|
+
readonly entryUrl: string;
|
|
19
|
+
/** 템플릿이 등록하는 custom element 이름. */
|
|
20
|
+
readonly elementName: string;
|
|
21
|
+
/** 문서 제목(placement 이름). */
|
|
22
|
+
readonly title: string;
|
|
23
|
+
/**
|
|
24
|
+
* 템플릿이 이미지·미디어를 불러올 추가 origin(정확한 https origin, 최대 `MAX_MEDIA_ORIGINS`개).
|
|
25
|
+
* CSP img-src·media-src에만 더해진다. 잘못된 항목이 하나라도 있으면 TypeError.
|
|
26
|
+
*/
|
|
27
|
+
readonly mediaOrigins?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/** mediaOrigins 최대 개수. */
|
|
30
|
+
declare const MAX_MEDIA_ORIGINS = 20;
|
|
31
|
+
/** elementName 최대 길이. 정규식 앞에서 먼저 잘라 입력 크기를 고정한다. */
|
|
32
|
+
declare const MAX_ELEMENT_NAME_LENGTH = 100;
|
|
33
|
+
/**
|
|
34
|
+
* 브리지 프로토콜 CSP. 값은 이스케이프 전 원문(호출자가 속성에 넣을 때 이스케이프).
|
|
35
|
+
*
|
|
36
|
+
* img-src·media-src에 맨 `https:`를 두지 않는다 — 두면 템플릿이 `<img src="https://attacker/?d=...">`로
|
|
37
|
+
* 임의 origin에 GET을 보낼 수 있다. 외부 이미지·미디어는 `mediaOrigins`로 정확한 origin만 연다.
|
|
38
|
+
*
|
|
39
|
+
* @throws TypeError mediaOrigins가 형식에 맞지 않을 때
|
|
40
|
+
*/
|
|
41
|
+
declare function buildFrameCsp(nonce: string, assetOrigin: string, mediaOrigins?: readonly string[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* 격리 프레임의 srcdoc 문서를 만든다. 모든 보간 값은 검증하고 문맥에 맞게 이스케이프한다
|
|
44
|
+
* (HTML 속성·텍스트는 HTML 이스케이프, 스크립트 안은 JSON + `<`·`>`·`&` 유니코드 이스케이프).
|
|
45
|
+
*
|
|
46
|
+
* iframe 속성은 `IFRAME_ATTRIBUTES`(sandbox="allow-scripts" 등)를 쓴다.
|
|
47
|
+
*
|
|
48
|
+
* @throws TypeError 입력이 형식에 맞지 않을 때(잘못된 nonce·세션·origin·엔트리·요소 이름·mediaOrigins)
|
|
49
|
+
*/
|
|
50
|
+
declare function createSrcdoc(options: CreateSrcdocOptions): string;
|
|
51
|
+
|
|
52
|
+
export { type CreateSrcdocOptions as C, MAX_ELEMENT_NAME_LENGTH as M, MAX_MEDIA_ORIGINS as a, buildFrameCsp as b, createNonce as c, createSession as d, createSrcdoc as e, isValidSession as f, isValidNonce as i };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** 프레임마다 새 세션(128비트 임의값)을 만든다. srcdoc 부트스트랩에만 심는다. */
|
|
2
|
+
declare function createSession(): string;
|
|
3
|
+
declare function isValidSession(value: unknown): value is string;
|
|
4
|
+
/** srcdoc 부트스트랩 스크립트용 CSP nonce(128비트, base64url)를 만든다. */
|
|
5
|
+
declare function createNonce(): string;
|
|
6
|
+
declare function isValidNonce(value: unknown): value is string;
|
|
7
|
+
|
|
8
|
+
interface CreateSrcdocOptions {
|
|
9
|
+
/** 부트스트랩 스크립트 CSP nonce(`createNonce()`). */
|
|
10
|
+
readonly nonce: string;
|
|
11
|
+
/** 프레임 세션(`createSession()`). */
|
|
12
|
+
readonly session: string;
|
|
13
|
+
/** 호스트 페이지 origin. 프레임 → 호스트 postMessage의 targetOrigin. */
|
|
14
|
+
readonly hostOrigin: string;
|
|
15
|
+
/** 템플릿 번들 CDN origin. CSP script·style·img·font·media 허용 대상. */
|
|
16
|
+
readonly assetOrigin: string;
|
|
17
|
+
/** 템플릿 엔트리 스크립트 URL. origin이 assetOrigin이어야 한다. */
|
|
18
|
+
readonly entryUrl: string;
|
|
19
|
+
/** 템플릿이 등록하는 custom element 이름. */
|
|
20
|
+
readonly elementName: string;
|
|
21
|
+
/** 문서 제목(placement 이름). */
|
|
22
|
+
readonly title: string;
|
|
23
|
+
/**
|
|
24
|
+
* 템플릿이 이미지·미디어를 불러올 추가 origin(정확한 https origin, 최대 `MAX_MEDIA_ORIGINS`개).
|
|
25
|
+
* CSP img-src·media-src에만 더해진다. 잘못된 항목이 하나라도 있으면 TypeError.
|
|
26
|
+
*/
|
|
27
|
+
readonly mediaOrigins?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/** mediaOrigins 최대 개수. */
|
|
30
|
+
declare const MAX_MEDIA_ORIGINS = 20;
|
|
31
|
+
/** elementName 최대 길이. 정규식 앞에서 먼저 잘라 입력 크기를 고정한다. */
|
|
32
|
+
declare const MAX_ELEMENT_NAME_LENGTH = 100;
|
|
33
|
+
/**
|
|
34
|
+
* 브리지 프로토콜 CSP. 값은 이스케이프 전 원문(호출자가 속성에 넣을 때 이스케이프).
|
|
35
|
+
*
|
|
36
|
+
* img-src·media-src에 맨 `https:`를 두지 않는다 — 두면 템플릿이 `<img src="https://attacker/?d=...">`로
|
|
37
|
+
* 임의 origin에 GET을 보낼 수 있다. 외부 이미지·미디어는 `mediaOrigins`로 정확한 origin만 연다.
|
|
38
|
+
*
|
|
39
|
+
* @throws TypeError mediaOrigins가 형식에 맞지 않을 때
|
|
40
|
+
*/
|
|
41
|
+
declare function buildFrameCsp(nonce: string, assetOrigin: string, mediaOrigins?: readonly string[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* 격리 프레임의 srcdoc 문서를 만든다. 모든 보간 값은 검증하고 문맥에 맞게 이스케이프한다
|
|
44
|
+
* (HTML 속성·텍스트는 HTML 이스케이프, 스크립트 안은 JSON + `<`·`>`·`&` 유니코드 이스케이프).
|
|
45
|
+
*
|
|
46
|
+
* iframe 속성은 `IFRAME_ATTRIBUTES`(sandbox="allow-scripts" 등)를 쓴다.
|
|
47
|
+
*
|
|
48
|
+
* @throws TypeError 입력이 형식에 맞지 않을 때(잘못된 nonce·세션·origin·엔트리·요소 이름·mediaOrigins)
|
|
49
|
+
*/
|
|
50
|
+
declare function createSrcdoc(options: CreateSrcdocOptions): string;
|
|
51
|
+
|
|
52
|
+
export { type CreateSrcdocOptions as C, MAX_ELEMENT_NAME_LENGTH as M, MAX_MEDIA_ORIGINS as a, buildFrameCsp as b, createNonce as c, createSession as d, createSrcdoc as e, isValidSession as f, isValidNonce as i };
|
package/dist/frame.cjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
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/frame/index.ts
|
|
21
|
+
var frame_exports = {};
|
|
22
|
+
__export(frame_exports, {
|
|
23
|
+
createBridgeClient: () => createBridgeClient,
|
|
24
|
+
createBridgeFetch: () => createBridgeFetch
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(frame_exports);
|
|
27
|
+
|
|
28
|
+
// src/frame/createBridgeFetch.ts
|
|
29
|
+
function createBridgeFetch(client) {
|
|
30
|
+
const NULL_BODY_STATUS = [204, 205, 304];
|
|
31
|
+
function toHeaderRecord(h) {
|
|
32
|
+
const out = {};
|
|
33
|
+
if (!h) return out;
|
|
34
|
+
new Headers(h).forEach(function(value, key) {
|
|
35
|
+
out[key] = value;
|
|
36
|
+
});
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function abortError(signal) {
|
|
40
|
+
if (signal.reason !== void 0) return signal.reason;
|
|
41
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
42
|
+
}
|
|
43
|
+
function bridgeFetch(input, init) {
|
|
44
|
+
return new Promise(function(resolve, reject) {
|
|
45
|
+
const opts = init || {};
|
|
46
|
+
const isRequest = typeof input === "object" && input !== null && typeof input.method === "string";
|
|
47
|
+
const url = isRequest ? input.url : String(input);
|
|
48
|
+
const method = String(opts.method || (isRequest ? input.method : "GET"));
|
|
49
|
+
const headers = toHeaderRecord(
|
|
50
|
+
opts.headers || (isRequest ? input.headers : null)
|
|
51
|
+
);
|
|
52
|
+
let body = null;
|
|
53
|
+
if (isRequest && input.body !== null && opts.body == null) {
|
|
54
|
+
reject(new TypeError("bridge fetch: pass the body via init.body"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const raw = opts.body;
|
|
58
|
+
if (raw === void 0 || raw === null) {
|
|
59
|
+
} else if (typeof raw === "string") {
|
|
60
|
+
body = raw;
|
|
61
|
+
} else if (typeof URLSearchParams !== "undefined" && raw instanceof URLSearchParams) {
|
|
62
|
+
body = raw.toString();
|
|
63
|
+
if (!headers["content-type"]) {
|
|
64
|
+
headers["content-type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
reject(new TypeError("bridge fetch: unsupported body type"));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const signal = opts.signal;
|
|
71
|
+
if (signal && signal.aborted) {
|
|
72
|
+
reject(abortError(signal));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let settled = false;
|
|
76
|
+
function onAbort() {
|
|
77
|
+
if (settled || !signal) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
reject(abortError(signal));
|
|
80
|
+
}
|
|
81
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
82
|
+
const payload = { url, method, headers, body };
|
|
83
|
+
client.request("fetch", payload).then(
|
|
84
|
+
function(res) {
|
|
85
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
try {
|
|
89
|
+
const nullBody = method.toUpperCase() === "HEAD" || NULL_BODY_STATUS.indexOf(res.status) >= 0;
|
|
90
|
+
const responseHeaders = {};
|
|
91
|
+
if (res.headers && typeof res.headers["content-type"] === "string") {
|
|
92
|
+
responseHeaders["content-type"] = res.headers["content-type"];
|
|
93
|
+
}
|
|
94
|
+
resolve(
|
|
95
|
+
new Response(nullBody ? null : typeof res.bodyText === "string" ? res.bodyText : "", {
|
|
96
|
+
status: res.status,
|
|
97
|
+
statusText: res.statusText,
|
|
98
|
+
headers: responseHeaders
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
reject(new TypeError("bridge fetch: invalid response"));
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
function(err) {
|
|
106
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
const code = err && typeof err === "object" ? err.code : void 0;
|
|
110
|
+
const failure = new TypeError("bridge fetch failed: " + String(code || "NETWORK_ERROR"));
|
|
111
|
+
failure.code = code || "NETWORK_ERROR";
|
|
112
|
+
reject(failure);
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return bridgeFetch;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/frame/runtime.ts
|
|
121
|
+
function bridgeRuntime(g, makeFetch, config) {
|
|
122
|
+
const NS = "bstage-bridge";
|
|
123
|
+
const TIMEOUT_MS = 15e3;
|
|
124
|
+
const READY_INTERVAL_MS = 250;
|
|
125
|
+
const READY_FAST_ATTEMPTS = 40;
|
|
126
|
+
const READY_SLOW_INTERVAL_MS = 2e3;
|
|
127
|
+
const MAX_ID = 64;
|
|
128
|
+
const MAX_TYPE = 32;
|
|
129
|
+
if (g.__bstage_bridge__) return g.__bstage_bridge__;
|
|
130
|
+
const session = config.session;
|
|
131
|
+
const hostOrigin = config.hostOrigin;
|
|
132
|
+
const parent = g.parent;
|
|
133
|
+
const pending = /* @__PURE__ */ new Map();
|
|
134
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
135
|
+
let init;
|
|
136
|
+
let seq = 0;
|
|
137
|
+
function randomPrefix() {
|
|
138
|
+
const bytes = new Uint8Array(8);
|
|
139
|
+
if (g.crypto && typeof g.crypto.getRandomValues === "function") {
|
|
140
|
+
g.crypto.getRandomValues(bytes);
|
|
141
|
+
} else {
|
|
142
|
+
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
143
|
+
}
|
|
144
|
+
let out = "";
|
|
145
|
+
for (let i = 0; i < bytes.length; i += 1) out += (bytes[i] + 256).toString(16).slice(1);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
const idPrefix = randomPrefix();
|
|
149
|
+
function nextId() {
|
|
150
|
+
seq += 1;
|
|
151
|
+
return idPrefix + "-" + seq;
|
|
152
|
+
}
|
|
153
|
+
function codeError(code, message) {
|
|
154
|
+
const err = new Error(message);
|
|
155
|
+
err.name = "BridgeError";
|
|
156
|
+
err.code = code;
|
|
157
|
+
return err;
|
|
158
|
+
}
|
|
159
|
+
function post(kind, type, id, payload) {
|
|
160
|
+
if (!parent) return;
|
|
161
|
+
const body = Object.assign({}, payload, { session });
|
|
162
|
+
parent.postMessage({ v: 1, ns: NS, id, kind, type, payload: body }, hostOrigin);
|
|
163
|
+
}
|
|
164
|
+
function isEnvelope(m) {
|
|
165
|
+
if (typeof m !== "object" || m === null) return false;
|
|
166
|
+
const e = m;
|
|
167
|
+
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;
|
|
168
|
+
}
|
|
169
|
+
let resolveReady = function() {
|
|
170
|
+
};
|
|
171
|
+
const ready = new Promise(function(resolve) {
|
|
172
|
+
resolveReady = resolve;
|
|
173
|
+
});
|
|
174
|
+
function emit(type, payload) {
|
|
175
|
+
const fns = listeners.get(type);
|
|
176
|
+
if (!fns) return;
|
|
177
|
+
fns.slice().forEach(function(fn) {
|
|
178
|
+
try {
|
|
179
|
+
fn(payload);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function applyEvent(type, payload) {
|
|
185
|
+
if (!init || typeof payload !== "object" || payload === null) return;
|
|
186
|
+
const p = payload;
|
|
187
|
+
if (type === "locale" && typeof p.locale === "string") {
|
|
188
|
+
init = Object.assign({}, init, { locale: p.locale, translations: p.translations });
|
|
189
|
+
} else if (type === "member" && typeof p.signedIn === "boolean") {
|
|
190
|
+
init = Object.assign({}, init, { member: payload });
|
|
191
|
+
} else if (type === "theme" && typeof p.dark === "boolean") {
|
|
192
|
+
init = Object.assign({}, init, { theme: payload });
|
|
193
|
+
} else if (type === "slot.context" && typeof p.id === "string") {
|
|
194
|
+
init = Object.assign({}, init, { slot: payload });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function onMessage(ev) {
|
|
198
|
+
if (!parent || ev.source !== parent) return;
|
|
199
|
+
if (ev.origin !== hostOrigin) return;
|
|
200
|
+
const m = ev.data;
|
|
201
|
+
if (!isEnvelope(m)) return;
|
|
202
|
+
const result = m.payload;
|
|
203
|
+
if (m.kind === "res" && m.type === "init") {
|
|
204
|
+
if (!init && result && result.ok === true && result.data) {
|
|
205
|
+
init = result.data;
|
|
206
|
+
clearInterval(readyTimer);
|
|
207
|
+
resolveReady(init);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (m.kind === "res") {
|
|
212
|
+
const entry = pending.get(m.id);
|
|
213
|
+
if (!entry) return;
|
|
214
|
+
pending.delete(m.id);
|
|
215
|
+
clearTimeout(entry.timer);
|
|
216
|
+
if (result && result.ok === true) {
|
|
217
|
+
entry.resolve(result.data);
|
|
218
|
+
} else {
|
|
219
|
+
const error = result && result.error || {};
|
|
220
|
+
entry.reject(
|
|
221
|
+
codeError(
|
|
222
|
+
typeof error.code === "string" ? error.code : "BAD_REQUEST",
|
|
223
|
+
typeof error.message === "string" ? error.message : "bridge request failed"
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (m.kind === "evt") {
|
|
230
|
+
applyEvent(m.type, m.payload);
|
|
231
|
+
emit(m.type, m.payload);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
g.addEventListener("message", onMessage);
|
|
235
|
+
let readyAttempts = 0;
|
|
236
|
+
function sendReady() {
|
|
237
|
+
readyAttempts += 1;
|
|
238
|
+
if (readyAttempts === READY_FAST_ATTEMPTS) {
|
|
239
|
+
clearInterval(readyTimer);
|
|
240
|
+
readyTimer = setInterval(sendReady, READY_SLOW_INTERVAL_MS);
|
|
241
|
+
}
|
|
242
|
+
post("evt", "ready", nextId(), {});
|
|
243
|
+
}
|
|
244
|
+
let readyTimer = setInterval(sendReady, READY_INTERVAL_MS);
|
|
245
|
+
sendReady();
|
|
246
|
+
function request(type, payload) {
|
|
247
|
+
return new Promise(function(resolve, reject) {
|
|
248
|
+
const id = nextId();
|
|
249
|
+
const timer = setTimeout(function() {
|
|
250
|
+
pending.delete(id);
|
|
251
|
+
reject(codeError("TIMEOUT", "bridge request timed out: " + type));
|
|
252
|
+
}, TIMEOUT_MS);
|
|
253
|
+
pending.set(id, { resolve, reject, timer });
|
|
254
|
+
post("req", type, id, payload || {});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function on(type, fn) {
|
|
258
|
+
const current = listeners.get(type) || [];
|
|
259
|
+
listeners.set(type, current.concat([fn]));
|
|
260
|
+
return function() {
|
|
261
|
+
const now = listeners.get(type) || [];
|
|
262
|
+
listeners.set(
|
|
263
|
+
type,
|
|
264
|
+
now.filter(function(f) {
|
|
265
|
+
return f !== fn;
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const base = {
|
|
271
|
+
version: 1,
|
|
272
|
+
hostOrigin,
|
|
273
|
+
ready,
|
|
274
|
+
getInit: function() {
|
|
275
|
+
return init;
|
|
276
|
+
},
|
|
277
|
+
request,
|
|
278
|
+
on
|
|
279
|
+
};
|
|
280
|
+
const client = Object.freeze(Object.assign({}, base, { fetch: makeFetch(base) }));
|
|
281
|
+
Object.defineProperty(g, "__bstage_bridge__", {
|
|
282
|
+
value: client,
|
|
283
|
+
writable: false,
|
|
284
|
+
configurable: false,
|
|
285
|
+
enumerable: false
|
|
286
|
+
});
|
|
287
|
+
return client;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/frame/createBridgeClient.ts
|
|
291
|
+
var BRIDGE_CONFIG_GLOBAL = "__bstage_bridge_config__";
|
|
292
|
+
function createBridgeClient(options = {}) {
|
|
293
|
+
const g = options.global ?? globalThis;
|
|
294
|
+
const installed = g[BRIDGE_CONFIG_GLOBAL] ?? {};
|
|
295
|
+
const session = options.session ?? installed.session;
|
|
296
|
+
const hostOrigin = options.hostOrigin ?? installed.hostOrigin;
|
|
297
|
+
if (typeof session !== "string" || typeof hostOrigin !== "string") {
|
|
298
|
+
throw new TypeError("[bstage-bridge] session\uACFC hostOrigin\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
299
|
+
}
|
|
300
|
+
return bridgeRuntime(g, createBridgeFetch, { session, hostOrigin });
|
|
301
|
+
}
|
|
302
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
303
|
+
0 && (module.exports = {
|
|
304
|
+
createBridgeClient,
|
|
305
|
+
createBridgeFetch
|
|
306
|
+
});
|