@bstage-sdk/bridge 0.9.1 → 0.11.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/frame.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var frame_exports = {};
22
22
  __export(frame_exports, {
23
23
  createBridgeClient: () => createBridgeClient,
24
- createBridgeFetch: () => createBridgeFetch
24
+ createBridgeFetch: () => createBridgeFetch,
25
+ installDirectFetch: () => installDirectFetch
25
26
  });
26
27
  module.exports = __toCommonJS(frame_exports);
27
28
 
@@ -130,6 +131,20 @@ function bridgeRuntime(g, makeFetch, config) {
130
131
  const session = config.session;
131
132
  const hostOrigin = config.hostOrigin;
132
133
  const parent = g.parent;
134
+ const sendToHost = parent ? parent.postMessage.bind(parent) : void 0;
135
+ const stopInterval = clearInterval;
136
+ const stopTimeout = clearTimeout;
137
+ let readPersisted;
138
+ const ownDescriptor = Object.getOwnPropertyDescriptor;
139
+ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
140
+ try {
141
+ const pte = g.PageTransitionEvent;
142
+ const desc = pte ? Object.getOwnPropertyDescriptor(pte.prototype, "persisted") : void 0;
143
+ if (desc && typeof desc.get === "function")
144
+ readPersisted = Function.prototype.call.bind(desc.get);
145
+ } catch {
146
+ readPersisted = void 0;
147
+ }
133
148
  const pending = /* @__PURE__ */ new Map();
134
149
  const listeners = /* @__PURE__ */ new Map();
135
150
  let init;
@@ -146,6 +161,17 @@ function bridgeRuntime(g, makeFetch, config) {
146
161
  return out;
147
162
  }
148
163
  const idPrefix = randomPrefix();
164
+ function secureToken() {
165
+ if (!g.crypto || typeof g.crypto.getRandomValues !== "function") return "";
166
+ const bytes = new Uint8Array(16);
167
+ g.crypto.getRandomValues(bytes);
168
+ let out = "";
169
+ for (let i = 0; i < bytes.length; i += 1) out += (bytes[i] + 256).toString(16).slice(1);
170
+ return out;
171
+ }
172
+ const frameToken = secureToken();
173
+ let closed = frameToken === "";
174
+ let unloaded = false;
149
175
  function nextId() {
150
176
  seq += 1;
151
177
  return idPrefix + "-" + seq;
@@ -157,9 +183,12 @@ function bridgeRuntime(g, makeFetch, config) {
157
183
  return err;
158
184
  }
159
185
  function post(kind, type, id, payload) {
160
- if (!parent) return;
186
+ if (!sendToHost || closed) return;
161
187
  const body = Object.assign({}, payload, { session });
162
- parent.postMessage({ v: 1, ns: NS, id, kind, type, payload: body }, hostOrigin);
188
+ sendToHost(
189
+ { v: 1, ns: NS, id, kind, type, payload: body, frame: frameToken },
190
+ hostOrigin
191
+ );
163
192
  }
164
193
  function isEnvelope(m) {
165
194
  if (typeof m !== "object" || m === null) return false;
@@ -232,6 +261,54 @@ function bridgeRuntime(g, makeFetch, config) {
232
261
  }
233
262
  }
234
263
  g.addEventListener("message", onMessage);
264
+ function isPersisted(ev) {
265
+ try {
266
+ if (readPersisted) return readPersisted(ev) === true;
267
+ if (typeof ev !== "object" || ev === null) return false;
268
+ const desc = ownDescriptor(ev, "persisted");
269
+ return !!desc && hasOwn(desc, "value") && desc.value === true;
270
+ } catch {
271
+ return false;
272
+ }
273
+ }
274
+ function onPageHide(ev) {
275
+ if (unloaded || isPersisted(ev)) return;
276
+ unloaded = true;
277
+ closed = true;
278
+ if (sendToHost) {
279
+ try {
280
+ sendToHost(
281
+ {
282
+ v: 1,
283
+ ns: NS,
284
+ id: idPrefix + "-unload",
285
+ kind: "evt",
286
+ type: "frame-unload",
287
+ payload: { session },
288
+ frame: frameToken
289
+ },
290
+ hostOrigin
291
+ );
292
+ } catch {
293
+ }
294
+ }
295
+ try {
296
+ stopInterval(readyTimer);
297
+ } catch {
298
+ }
299
+ try {
300
+ pending.forEach(function(entry) {
301
+ try {
302
+ stopTimeout(entry.timer);
303
+ entry.reject(codeError("UNSUPPORTED", "bridge closed"));
304
+ } catch {
305
+ }
306
+ });
307
+ pending.clear();
308
+ } catch {
309
+ }
310
+ }
311
+ g.addEventListener("pagehide", onPageHide, true);
235
312
  let readyAttempts = 0;
236
313
  function sendReady() {
237
314
  readyAttempts += 1;
@@ -245,6 +322,10 @@ function bridgeRuntime(g, makeFetch, config) {
245
322
  sendReady();
246
323
  function request(type, payload) {
247
324
  return new Promise(function(resolve, reject) {
325
+ if (closed) {
326
+ reject(codeError("UNSUPPORTED", "bridge closed"));
327
+ return;
328
+ }
248
329
  const id = nextId();
249
330
  const timer = setTimeout(function() {
250
331
  pending.delete(id);
@@ -299,8 +380,129 @@ function createBridgeClient(options = {}) {
299
380
  }
300
381
  return bridgeRuntime(g, createBridgeFetch, { session, hostOrigin });
301
382
  }
383
+
384
+ // src/frame/directFetch.ts
385
+ function installDirectFetch(g, client, config) {
386
+ const hostOrigin = config.hostOrigin;
387
+ const callFn = Function.prototype.call;
388
+ const NativeURL = g.URL;
389
+ const rawFetch = g.fetch;
390
+ const nativeFetch = typeof rawFetch === "function" ? callFn.bind(rawFetch) : void 0;
391
+ const bridgeFetch = client.fetch;
392
+ const strSlice = callFn.bind(String.prototype.slice);
393
+ const promiseThen = callFn.bind(Promise.prototype.then);
394
+ const promiseReject = Promise.reject.bind(Promise);
395
+ const assign = Object.assign;
396
+ const TypeErr = TypeError;
397
+ function getter(ctor, name) {
398
+ if (typeof ctor !== "function") return void 0;
399
+ let proto = ctor.prototype;
400
+ while (proto) {
401
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
402
+ if (desc && typeof desc.get === "function") {
403
+ return callFn.bind(desc.get);
404
+ }
405
+ proto = Object.getPrototypeOf(proto);
406
+ }
407
+ return void 0;
408
+ }
409
+ const getHref = getter(NativeURL, "href");
410
+ const getOrigin = getter(NativeURL, "origin");
411
+ const getProtocol = getter(NativeURL, "protocol");
412
+ const getPathname = getter(NativeURL, "pathname");
413
+ const getRequestUrl = getter(g.Request, "url");
414
+ let gatewayPrefixes = null;
415
+ let partnerOrigins = [];
416
+ let routesReady = false;
417
+ function copyStrings(value) {
418
+ if (!Array.isArray(value)) return null;
419
+ const out = [];
420
+ for (let i = 0; i < value.length; i += 1) {
421
+ if (typeof value[i] === "string") out[out.length] = value[i];
422
+ }
423
+ return out;
424
+ }
425
+ function takeSnapshot(init) {
426
+ if (routesReady) return;
427
+ const raw = init;
428
+ gatewayPrefixes = copyStrings(raw.gatewayPrefixes);
429
+ partnerOrigins = copyStrings(raw.partnerOrigins) || [];
430
+ routesReady = true;
431
+ }
432
+ promiseThen(client.ready, takeSnapshot);
433
+ function matchesPrefix(pathname, prefix) {
434
+ const bare = strSlice(prefix, prefix.length - 1) === "/" ? strSlice(prefix, 0, -1) : prefix;
435
+ return pathname === bare || strSlice(pathname, 0, bare.length + 1) === bare + "/";
436
+ }
437
+ function routeOf(origin, protocol, pathname) {
438
+ if (origin === hostOrigin) {
439
+ if (gatewayPrefixes === null) return "bridge";
440
+ for (let i = 0; i < gatewayPrefixes.length; i += 1) {
441
+ if (matchesPrefix(pathname, gatewayPrefixes[i])) return "bridge";
442
+ }
443
+ return "reject";
444
+ }
445
+ for (let i = 0; i < partnerOrigins.length; i += 1) {
446
+ if (origin === partnerOrigins[i]) return "bridge";
447
+ }
448
+ return protocol === "https:" ? "direct" : "reject";
449
+ }
450
+ function requestUrlOf(input) {
451
+ if (!getRequestUrl || typeof input !== "object" || input === null) return void 0;
452
+ try {
453
+ return getRequestUrl(input);
454
+ } catch {
455
+ return void 0;
456
+ }
457
+ }
458
+ function dispatch(input, init) {
459
+ if (typeof NativeURL !== "function" || !getHref || !getOrigin || !getProtocol || !getPathname) {
460
+ return promiseReject(new TypeErr("bstage fetch: URL is unavailable"));
461
+ }
462
+ const requestUrl = requestUrlOf(input);
463
+ const request = requestUrl !== void 0;
464
+ let parsed;
465
+ try {
466
+ parsed = new NativeURL(request ? requestUrl : String(input), hostOrigin);
467
+ } catch {
468
+ return promiseReject(new TypeErr("bstage fetch: invalid url"));
469
+ }
470
+ const href = getHref(parsed);
471
+ const route = routeOf(getOrigin(parsed), getProtocol(parsed), getPathname(parsed));
472
+ const target = request ? input : href;
473
+ if (route === "bridge") return bridgeFetch(target, init);
474
+ if (route === "reject") {
475
+ return promiseReject(new TypeErr("bstage fetch: destination not allowed"));
476
+ }
477
+ if (!nativeFetch) return promiseReject(new TypeErr("bstage fetch: fetch is unavailable"));
478
+ return nativeFetch(
479
+ g,
480
+ target,
481
+ assign({}, init, { credentials: "omit", referrerPolicy: "no-referrer" })
482
+ );
483
+ }
484
+ function bstageFetch(input, init) {
485
+ try {
486
+ if (routesReady) return dispatch(input, init);
487
+ return promiseThen(client.ready, function(value) {
488
+ takeSnapshot(value);
489
+ return dispatch(input, init);
490
+ });
491
+ } catch {
492
+ return promiseReject(new TypeErr("bstage fetch: request failed"));
493
+ }
494
+ }
495
+ Object.defineProperty(g, "__bstage_fetch__", {
496
+ value: bstageFetch,
497
+ writable: false,
498
+ configurable: false,
499
+ enumerable: false
500
+ });
501
+ return bstageFetch;
502
+ }
302
503
  // Annotate the CommonJS export names for ESM import in node:
303
504
  0 && (module.exports = {
304
505
  createBridgeClient,
305
- createBridgeFetch
506
+ createBridgeFetch,
507
+ installDirectFetch
306
508
  });
package/dist/frame.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-9N0GFq5b.cjs';
1
+ import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-BjpZyiLy.cjs';
2
2
 
3
3
  /** 프레임 런타임 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
4
4
  interface BridgeClientConfig {
@@ -28,14 +28,18 @@ interface RuntimeGlobal {
28
28
  parent: {
29
29
  postMessage(message: unknown, targetOrigin: string): void;
30
30
  } | null;
31
- addEventListener(type: 'message', fn: (ev: {
32
- source: unknown;
33
- origin: string;
34
- data: unknown;
35
- }) => void): void;
31
+ /**
32
+ * `message`(호스트 응답·이벤트)와 `pagehide`(frame-unload 전송, capture)에 쓴다.
33
+ * 처리기 인자 모양은 type마다 다르다 — message는 `{ source, origin, data }`, pagehide는 `{ persisted }`.
34
+ */
35
+ addEventListener(type: 'message' | 'pagehide', fn: (ev: never) => void, capture?: boolean): void;
36
36
  crypto?: {
37
37
  getRandomValues<T extends ArrayBufferView | null>(array: T): T;
38
38
  };
39
+ /** pagehide 이벤트 생성자 — `persisted` 원본 접근자를 부트스트랩 때 잡는 데 쓴다(없으면 이벤트 속성을 읽는다). */
40
+ PageTransitionEvent?: {
41
+ readonly prototype: object;
42
+ };
39
43
  __bstage_bridge__?: BridgeClient;
40
44
  }
41
45
  /** `createBridgeFetch`가 요구하는 클라이언트 부분. */
@@ -69,4 +73,45 @@ declare function createBridgeClient(options?: CreateBridgeClientOptions): Bridge
69
73
  */
70
74
  declare function createBridgeFetch(client: BridgeRequester): typeof globalThis.fetch;
71
75
 
72
- export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type Unsubscribe, createBridgeClient, createBridgeFetch };
76
+ /** `installDirectFetch` 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
77
+ interface DirectFetchConfig {
78
+ /** 호스트 페이지 origin(srcdoc에 심긴 값). 상대 URL의 기준이자 "같은 origin" 판정 기준. */
79
+ readonly hostOrigin: string;
80
+ }
81
+ /** 프레임 fetch 라우터가 쓰는 전역의 최소 모양 — 테스트에서 가짜 창을 넘길 수 있게 한다. */
82
+ interface DirectFetchGlobal {
83
+ fetch?: typeof globalThis.fetch;
84
+ URL?: typeof URL;
85
+ Request?: typeof Request;
86
+ __bstage_fetch__?: typeof globalThis.fetch;
87
+ }
88
+ /** 갈래 판정 결과. */
89
+ type DirectRoute = 'bridge' | 'direct' | 'reject';
90
+ /**
91
+ * 격리 프레임의 fetch 라우터를(`createSrcdoc` 부트스트랩이 항상 설치한다, README "프레임 네트워크")
92
+ * `globalThis.__bstage_fetch__`에 설치한다. inline 모드용으로 쓰인 템플릿이 부르는 그 이름이다.
93
+ *
94
+ * 갈래(`new URL(input, hostOrigin)`으로 정규화한 뒤):
95
+ * - **브리지**(토큰이 필요한 목적지): hostOrigin과 같은 origin이고 경로가 init `gatewayPrefixes` 안, 또는
96
+ * origin이 init `partnerOrigins`와 정확히 일치. 기존 브리지 fetch(`client.fetch`)로 보내고 호스트가 다시 판정한다.
97
+ * - **직접**: 그 밖의 https. 부트스트랩 때 잡아 둔 프레임의 원래 `fetch`로 `credentials: 'omit'`·
98
+ * `referrerPolicy: 'no-referrer'`를 덮어 부른다. 인증 헤더는 붙이지 않는다(토큰은 프레임에 없다).
99
+ * - **거절**(`TypeError`): hostOrigin의 게이트웨이 밖 경로(혼동 방지 — 직접 보내지 않는다), https가 아닌 스킴.
100
+ *
101
+ * **자기완결 함수다** — `bridgeRuntime`과 함께 srcdoc에 `toString()`으로 심긴다(runtime.ts 주석 참고).
102
+ * 템플릿 코드보다 먼저 돌고, 판정·전송에 쓰는 전역(`fetch`·`URL`과 그 접근자·`String.prototype.slice`·
103
+ * `Promise.prototype.then`·`Object.assign`·`TypeError`)과 init의 라우팅 목록을 이때 잡아 둔다. 그래서 템플릿이
104
+ * 나중에 전역·프로토타입·init 객체를 바꿔도 갈래가 바뀌지 않는다. 갈래가 바뀌어도 토큰은 새지 않는다 —
105
+ * 브리지로 간 요청은 호스트가 따로 판정하고, 직접 간 요청에는 토큰이 없다. 이 고정은 혼동 방지다.
106
+ *
107
+ * init이 `gatewayPrefixes`를 싣지 않으면(옛 호스트) hostOrigin 요청은 경로와 무관하게 브리지로 보낸다
108
+ * (호스트가 판정하므로 안전한 쪽).
109
+ *
110
+ * @param g 프레임 전역(`globalThis`)
111
+ * @param client 프레임 브리지 클라이언트(`bridgeRuntime` 결과, frozen)
112
+ * @param config srcdoc에 심긴 hostOrigin
113
+ * @returns 설치한 fetch 라우터
114
+ */
115
+ declare function installDirectFetch(g: DirectFetchGlobal, client: Pick<BridgeClient, 'fetch' | 'ready'>, config: DirectFetchConfig): typeof globalThis.fetch;
116
+
117
+ export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type DirectFetchConfig, type DirectFetchGlobal, type DirectRoute, type Unsubscribe, createBridgeClient, createBridgeFetch, installDirectFetch };
package/dist/frame.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-9N0GFq5b.js';
1
+ import { g as BridgeInit, j as BridgeRequestType, i as BridgeRequestMap, e as BridgeEventMap } from './messages-BjpZyiLy.js';
2
2
 
3
3
  /** 프레임 런타임 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
4
4
  interface BridgeClientConfig {
@@ -28,14 +28,18 @@ interface RuntimeGlobal {
28
28
  parent: {
29
29
  postMessage(message: unknown, targetOrigin: string): void;
30
30
  } | null;
31
- addEventListener(type: 'message', fn: (ev: {
32
- source: unknown;
33
- origin: string;
34
- data: unknown;
35
- }) => void): void;
31
+ /**
32
+ * `message`(호스트 응답·이벤트)와 `pagehide`(frame-unload 전송, capture)에 쓴다.
33
+ * 처리기 인자 모양은 type마다 다르다 — message는 `{ source, origin, data }`, pagehide는 `{ persisted }`.
34
+ */
35
+ addEventListener(type: 'message' | 'pagehide', fn: (ev: never) => void, capture?: boolean): void;
36
36
  crypto?: {
37
37
  getRandomValues<T extends ArrayBufferView | null>(array: T): T;
38
38
  };
39
+ /** pagehide 이벤트 생성자 — `persisted` 원본 접근자를 부트스트랩 때 잡는 데 쓴다(없으면 이벤트 속성을 읽는다). */
40
+ PageTransitionEvent?: {
41
+ readonly prototype: object;
42
+ };
39
43
  __bstage_bridge__?: BridgeClient;
40
44
  }
41
45
  /** `createBridgeFetch`가 요구하는 클라이언트 부분. */
@@ -69,4 +73,45 @@ declare function createBridgeClient(options?: CreateBridgeClientOptions): Bridge
69
73
  */
70
74
  declare function createBridgeFetch(client: BridgeRequester): typeof globalThis.fetch;
71
75
 
72
- export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type Unsubscribe, createBridgeClient, createBridgeFetch };
76
+ /** `installDirectFetch` 설정 — srcdoc 부트스트랩이 상수로 넘긴다. */
77
+ interface DirectFetchConfig {
78
+ /** 호스트 페이지 origin(srcdoc에 심긴 값). 상대 URL의 기준이자 "같은 origin" 판정 기준. */
79
+ readonly hostOrigin: string;
80
+ }
81
+ /** 프레임 fetch 라우터가 쓰는 전역의 최소 모양 — 테스트에서 가짜 창을 넘길 수 있게 한다. */
82
+ interface DirectFetchGlobal {
83
+ fetch?: typeof globalThis.fetch;
84
+ URL?: typeof URL;
85
+ Request?: typeof Request;
86
+ __bstage_fetch__?: typeof globalThis.fetch;
87
+ }
88
+ /** 갈래 판정 결과. */
89
+ type DirectRoute = 'bridge' | 'direct' | 'reject';
90
+ /**
91
+ * 격리 프레임의 fetch 라우터를(`createSrcdoc` 부트스트랩이 항상 설치한다, README "프레임 네트워크")
92
+ * `globalThis.__bstage_fetch__`에 설치한다. inline 모드용으로 쓰인 템플릿이 부르는 그 이름이다.
93
+ *
94
+ * 갈래(`new URL(input, hostOrigin)`으로 정규화한 뒤):
95
+ * - **브리지**(토큰이 필요한 목적지): hostOrigin과 같은 origin이고 경로가 init `gatewayPrefixes` 안, 또는
96
+ * origin이 init `partnerOrigins`와 정확히 일치. 기존 브리지 fetch(`client.fetch`)로 보내고 호스트가 다시 판정한다.
97
+ * - **직접**: 그 밖의 https. 부트스트랩 때 잡아 둔 프레임의 원래 `fetch`로 `credentials: 'omit'`·
98
+ * `referrerPolicy: 'no-referrer'`를 덮어 부른다. 인증 헤더는 붙이지 않는다(토큰은 프레임에 없다).
99
+ * - **거절**(`TypeError`): hostOrigin의 게이트웨이 밖 경로(혼동 방지 — 직접 보내지 않는다), https가 아닌 스킴.
100
+ *
101
+ * **자기완결 함수다** — `bridgeRuntime`과 함께 srcdoc에 `toString()`으로 심긴다(runtime.ts 주석 참고).
102
+ * 템플릿 코드보다 먼저 돌고, 판정·전송에 쓰는 전역(`fetch`·`URL`과 그 접근자·`String.prototype.slice`·
103
+ * `Promise.prototype.then`·`Object.assign`·`TypeError`)과 init의 라우팅 목록을 이때 잡아 둔다. 그래서 템플릿이
104
+ * 나중에 전역·프로토타입·init 객체를 바꿔도 갈래가 바뀌지 않는다. 갈래가 바뀌어도 토큰은 새지 않는다 —
105
+ * 브리지로 간 요청은 호스트가 따로 판정하고, 직접 간 요청에는 토큰이 없다. 이 고정은 혼동 방지다.
106
+ *
107
+ * init이 `gatewayPrefixes`를 싣지 않으면(옛 호스트) hostOrigin 요청은 경로와 무관하게 브리지로 보낸다
108
+ * (호스트가 판정하므로 안전한 쪽).
109
+ *
110
+ * @param g 프레임 전역(`globalThis`)
111
+ * @param client 프레임 브리지 클라이언트(`bridgeRuntime` 결과, frozen)
112
+ * @param config srcdoc에 심긴 hostOrigin
113
+ * @returns 설치한 fetch 라우터
114
+ */
115
+ declare function installDirectFetch(g: DirectFetchGlobal, client: Pick<BridgeClient, 'fetch' | 'ready'>, config: DirectFetchConfig): typeof globalThis.fetch;
116
+
117
+ export { type BridgeClient, type BridgeClientConfig, type BridgeRequester, type CreateBridgeClientOptions, type DirectFetchConfig, type DirectFetchGlobal, type DirectRoute, type Unsubscribe, createBridgeClient, createBridgeFetch, installDirectFetch };
package/dist/frame.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  bridgeRuntime,
3
- createBridgeFetch
4
- } from "./chunk-KQMJY4ZH.js";
3
+ createBridgeFetch,
4
+ installDirectFetch
5
+ } from "./chunk-CSAW3L7P.js";
5
6
 
6
7
  // src/frame/createBridgeClient.ts
7
8
  var BRIDGE_CONFIG_GLOBAL = "__bstage_bridge_config__";
@@ -17,5 +18,6 @@ function createBridgeClient(options = {}) {
17
18
  }
18
19
  export {
19
20
  createBridgeClient,
20
- createBridgeFetch
21
+ createBridgeFetch,
22
+ installDirectFetch
21
23
  };