@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bemyfriends Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,195 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bstage-sdk/bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
템플릿을 sandbox iframe에 격리해 실행할 때 쓰는 브리지. 플랫폼(호스트)과 프레임 사이의 메시지 프로토콜,
|
|
4
|
+
호스트 측 요청 정책, 프레임 측 클라이언트를 담는다. 런타임 의존성은 없다.
|
|
5
|
+
|
|
6
|
+
격리는 **placement 단위 선택 기능**이다. 켜지 않은 placement는 지금처럼 inline(Shadow DOM)으로 돈다.
|
|
7
|
+
템플릿 코드는 바꿀 필요 없이 이 브리지를 지원하는 core·react로 다시 빌드하면 된다 — core·react가 프레임 안인지(`globalThis.__bstage_bridge__`)를 스스로
|
|
8
|
+
판별해 API 호출·로케일·네비게이션을 브리지로 보낸다.
|
|
9
|
+
|
|
10
|
+
## 진입점
|
|
11
|
+
|
|
12
|
+
| 경로 | 쓰는 쪽 | 내용 |
|
|
13
|
+
| --------- | --------------------- | --------------------------------------------------------------------------------- |
|
|
14
|
+
| `.` | 공용 | 봉투·메시지 타입과 가드, 오류 코드, `createSession`·`createNonce`, `createSrcdoc` |
|
|
15
|
+
| `./host` | 플랫폼(호스트 페이지) | `createBridgeHost`, fetch 정책 기본값 |
|
|
16
|
+
| `./frame` | 프레임 안 | `createBridgeClient`, `createBridgeFetch` |
|
|
17
|
+
|
|
18
|
+
## 호스트에서
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createNonce,
|
|
23
|
+
createSession,
|
|
24
|
+
createSrcdoc,
|
|
25
|
+
IFRAME_ATTRIBUTES,
|
|
26
|
+
} from '@bstage-sdk/bridge'
|
|
27
|
+
import { createBridgeHost, DEFAULT_GATEWAY_DENY_SEGMENTS } from '@bstage-sdk/bridge/host'
|
|
28
|
+
|
|
29
|
+
const session = createSession()
|
|
30
|
+
const iframe = document.createElement('iframe')
|
|
31
|
+
iframe.setAttribute('sandbox', IFRAME_ATTRIBUTES.sandbox)
|
|
32
|
+
iframe.setAttribute('allow', IFRAME_ATTRIBUTES.allow)
|
|
33
|
+
iframe.setAttribute('referrerpolicy', IFRAME_ATTRIBUTES.referrerpolicy)
|
|
34
|
+
iframe.title = placementName
|
|
35
|
+
|
|
36
|
+
const host = createBridgeHost({
|
|
37
|
+
iframe,
|
|
38
|
+
session,
|
|
39
|
+
hostOrigin: location.origin,
|
|
40
|
+
init: {
|
|
41
|
+
placementId, // 호스트가 만든 스코프 키 — 예: `page:{경로}|{url}`, `slot:{id}|{url}`
|
|
42
|
+
surface: 'page', // 'page' | 'slot'
|
|
43
|
+
phase,
|
|
44
|
+
spaceId,
|
|
45
|
+
locale,
|
|
46
|
+
theme: { dark },
|
|
47
|
+
member: { signedIn },
|
|
48
|
+
partnerOrigins, // 팬 토큰을 보낼 vibe 백엔드 목록(토큰 없이 부를 외부 API는 넣지 않는다)
|
|
49
|
+
translations, // 선택: 현재 locale 번역 사전
|
|
50
|
+
},
|
|
51
|
+
fetchImpl: platformFetch, // 같은 origin /gw — 인증·목적지 허용 목록을 적용하는 플랫폼 fetch
|
|
52
|
+
partnerFetchImpl, // 선택: partnerOrigins용(토큰을 붙일지는 호스트가 결정)
|
|
53
|
+
// externalFetchImpl, // 선택: 그 밖의 https용. 기본은 무인증 fetch
|
|
54
|
+
pinnedHeaders: { 'x-bstage-tenant-id': spaceId },
|
|
55
|
+
policy: {
|
|
56
|
+
gatewayDenySegments: DEFAULT_GATEWAY_DENY_SEGMENTS, // 정책을 코드에 드러낸다(필요하면 [...상수, '추가'])
|
|
57
|
+
allowExternal: true, // 외부 https 갈래는 옵트인(기본 false). 미리보기처럼 막을 호스트는 생략
|
|
58
|
+
},
|
|
59
|
+
handlers: { navigate, back, toast, resize, track },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
iframe.srcdoc = createSrcdoc({
|
|
63
|
+
nonce: createNonce(),
|
|
64
|
+
session,
|
|
65
|
+
hostOrigin: location.origin,
|
|
66
|
+
assetOrigin,
|
|
67
|
+
entryUrl,
|
|
68
|
+
elementName,
|
|
69
|
+
title: placementName,
|
|
70
|
+
mediaOrigins, // 선택: 템플릿이 이미지·미디어를 불러올 정확한 https origin(최대 20개)
|
|
71
|
+
})
|
|
72
|
+
container.appendChild(iframe)
|
|
73
|
+
|
|
74
|
+
// 언마운트 시
|
|
75
|
+
host.dispose()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
호스트는 `ev.source === iframe.contentWindow`, origin `"null"`, 봉투 형식, `payload.session === session`이
|
|
79
|
+
모두 맞는 메시지만 처리한다. 토큰은 프레임으로 가지 않는다 — 인증은 `fetchImpl`이 호스트 쪽에서 붙이고,
|
|
80
|
+
프레임에는 상태·statusText·content-type·본문 문자열만 돌아간다.
|
|
81
|
+
|
|
82
|
+
### fetch 정책
|
|
83
|
+
|
|
84
|
+
`new URL(url, hostOrigin)`으로 정규화한 뒤 목적지를 네 갈래로 나눈다.
|
|
85
|
+
|
|
86
|
+
| 갈래 | 조건 | fetch | 자격증명·헤더 |
|
|
87
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------- |
|
|
88
|
+
| `gateway` | 같은 origin, 경로가 `gatewayPrefixes`(기본 `["/gw/"]`, `/gw` 정확 일치 포함) 안·거절 규칙 밖 | `fetchImpl` | `credentials: 'same-origin'`, 허용 헤더 + `pinnedHeaders` |
|
|
89
|
+
| `partner` | `partnerOrigins`(**팬 토큰을 보낼 vibe 백엔드**, https 정확한 origin, 최대 10개, hostOrigin 제외)와 정확히 일치 | `partnerFetchImpl` | `credentials: 'omit'`, `referrerPolicy: 'no-referrer'`, 허용 헤더만 |
|
|
90
|
+
| `external` | 그 밖의 https origin(hostOrigin 아님). **등록 불필요**. `policy.allowExternal: true`일 때만(기본 `false`, 옵트인) | `externalFetchImpl` | `credentials: 'omit'`, `referrerPolicy: 'no-referrer'`, 허용 헤더만 |
|
|
91
|
+
| 거절 | http, userinfo, 비 http 스킴, 같은 origin의 `/gw` 밖·거절 경로, `allowExternal`을 켜지 않았을 때의 external | — | `FORBIDDEN_DESTINATION` |
|
|
92
|
+
|
|
93
|
+
- 세 갈래 모두 `redirect: 'error'`, 메서드·본문 상한·분당/동시 요청 상한·응답 본문 상한이 같게 적용된다.
|
|
94
|
+
- 같은 origin은 gateway 규칙만 적용한다. hostOrigin을 파트너 목록에 넣어도 `/gw` 밖 경로는 열리지 않고, external로도 통과하지 않는다.
|
|
95
|
+
- `pinnedHeaders`(테넌시·앱 헤더)는 **gateway에만** 싣는다. partner·external에는 프레임이 보낸 허용 헤더만 간다.
|
|
96
|
+
- external·partner 기본 fetch는 `globalThis.fetch`를 쿠키·인증 헤더·Referer 없이 부른다. 응답은 호스트 페이지 origin 기준 CORS를 따른다.
|
|
97
|
+
- external은 요청이 사용자 브라우저에서 호스트 `Origin`으로 나가므로 **IP 리터럴·단일 레이블·빈 레이블·로컬/예약 이름(`localhost`·`local`·`internal`·`home.arpa`·`test`·`invalid`·`example`·`lan`·`corp`·`intranet`·`localdomain`과 그 하위)·
|
|
98
|
+
호스트와 같은 호스트명(다른 포트·끝 점 몇 개든)**을 겨누지 못한다(사설 IP로 풀리는 DNS 이름은 브라우저의 Local Network Access가 다룬다).
|
|
99
|
+
`x-bmf-sdk-version` 같은 비단순 헤더를 external에 실으면 CORS preflight가 생겨 공개 API가 거절할 수 있다.
|
|
100
|
+
- `externalFetchImpl`에 `fetchImpl`(인증 fetch)이나 `partnerFetchImpl`(팬 토큰 fetch)을 그대로 넘기면 생성 시 `TypeError`.
|
|
101
|
+
- `partnerFetchImpl`에 `fetchImpl`(게이트웨이 인증 fetch)을 그대로 넘겨도 생성 시 `TypeError` — 파트너 갈래에는 게이트웨이 경로 거절 규칙이 적용되지 않는다.
|
|
102
|
+
- **알려진 한계(external)**: 호스트명 검사는 이름만 본다. ① 같은 사이트의 다른 호스트(예: 호스트가 `space.example.com`일 때 `api.example.com`)로의
|
|
103
|
+
무인증 요청은 막지 않는다 — 쿠키·인증 헤더는 싣지 않지만, 그 호스트가 호스트 origin에 CORS를 열어 두었다면 응답을 읽을 수 있다.
|
|
104
|
+
② 공개 DNS 이름이 사설·루프백 IP로 풀리는 경우(DNS rebinding 포함)는 SDK가 판정하지 못하고 브라우저의 Local Network Access에 맡긴다.
|
|
105
|
+
둘 다 막아야 하는 호스트는 `allowExternal`을 끄거나 `externalFetchImpl`에서 목적지를 직접 걸러야 한다.
|
|
106
|
+
- 같은 origin 경로 구조 검사(navigate와 공용): 원문을 `decodeURIComponent`로 최대 3회 고정점까지 풀고,
|
|
107
|
+
디코딩 실패(overlong·잘못된 UTF-8, 예: `%c0%af`)·고정점 미도달이거나 어느 단계에든 역슬래시·`;`·NUL/제어문자·
|
|
108
|
+
점 세그먼트(`/../`·`/./`·끝의 `/..`·`/.`)가 있으면 거절한다. 세그먼트 안 정상 인코딩(`/gw/x/a%2Fb`)은 통과.
|
|
109
|
+
navigate는 여기에 `//` 금지와 `new URL(path, hostOrigin).origin === hostOrigin`을 더한다.
|
|
110
|
+
- **게이트웨이 거절 접두사** `gatewayDenyPrefixes`(기본 `["/gw/account/v1/auth/", "/gw/account/v1/users/logout"]`):
|
|
111
|
+
토큰 갱신은 refresh 쿠키로 새 access token을 응답 본문에 돌려주므로 프레임이 닿으면 안 된다. 디코딩·슬래시 병합·
|
|
112
|
+
소문자화한 모든 단계로 비교하며, 걸리면 `FORBIDDEN_DESTINATION`. 여기에 **거절 세그먼트** `gatewayDenySegments`
|
|
113
|
+
(기본 `DEFAULT_GATEWAY_DENY_SEGMENTS` = `auth`·`logout`·`mfa`·`token`·`oauth`·`sso`·`debug`·`debugger`·`xdebug`·`withdraw`)를
|
|
114
|
+
경로 어디에 있든 막는다(호스트가 넘긴 값은 소문자로 정규화해 비교) — 실제 인증 경로는 `/gw/svc/account/api/v1/auth/token/refresh`처럼 서비스·버전 접두사가 붙기 때문이다.
|
|
115
|
+
기본값은 패키지 버전마다 늘 수 있다. 호스트는 상수를 명시해 넘겨 정책을 코드에 드러내고, 고정이 필요하면 리터럴 목록을 넘긴다. 이와 별개로 **`token`이 들어간 세그먼트**
|
|
116
|
+
(`tokens`·`access-tokens`·`refreshToken`)는 정책과 무관하게 항상 거절한다 — `device-tokens`·`tokenization` 같은 정상 API도
|
|
117
|
+
격리 프레임에서는 부를 수 없다(수용한 제약). 거절 규칙은 gateway 경로에만 적용한다.
|
|
118
|
+
- userinfo가 붙은 URL은 거절한다.
|
|
119
|
+
- 메서드는 GET·HEAD·POST·PUT·PATCH·DELETE.
|
|
120
|
+
- 요청 헤더는 `content-type`·`accept`·`accept-language`, 앱 헤더 `x-bstage-app-id`·`x-bstage-app-key`, `x-bmf-sdk-version`만 프레임 값을
|
|
121
|
+
통과시킨다(대소문자 무시, 세 갈래 공통). **테넌시 헤더(`x-bstage-tenant-id`·`x-bstage-org-id`·`x-bstage-enterprise-id`)는 프레임 값을
|
|
122
|
+
버리고** 호스트가 `pinnedHeaders`로 준 값만 게이트웨이 요청에 싣는다(앱 헤더도 여기 주면 프레임 값을 덮는다).
|
|
123
|
+
`authorization`·`cookie`·(`x-bmf-sdk-version` 밖의) `x-bmf-*` 등 나머지는 버린다. 템플릿의 `BstageClient`가 테넌트 헤더를 보내므로 호스트는
|
|
124
|
+
`pinnedHeaders: { 'x-bstage-tenant-id': spaceId }`처럼 현재 스페이스 값을 고정해야 한다.
|
|
125
|
+
- 본문은 문자열만, UTF-8 바이트로 `maxBodyBytes`(기본 1MB) 이하. 프레임별 분당 `maxRequestsPerMinute`(기본 300,
|
|
126
|
+
resize 제외), 동시 진행 fetch 16개, toast 분당 5회, ready 응답 초당 2회, resize 초당 30회.
|
|
127
|
+
- 응답 본문은 스트림으로 읽으며 `maxResponseBytes`(기본 10MB)를 넘는 순간 취소한다.
|
|
128
|
+
- 호출 인자: gateway는 `fetchImpl(url, { method, headers, body, credentials: 'same-origin', redirect: 'error' })`,
|
|
129
|
+
partner·external은 `partnerFetchImpl`·`externalFetchImpl(url, { ..., credentials: 'omit', redirect: 'error', referrerPolicy: 'no-referrer' })`.
|
|
130
|
+
|
|
131
|
+
### 파트너·외부 fetch
|
|
132
|
+
|
|
133
|
+
`fetchImpl`(플랫폼 인증 fetch)은 **같은 origin `/gw` 요청에만** 쓴다 — `/gw` 밖 같은 origin 경로에 쓰지 않는다
|
|
134
|
+
(정책이 그런 요청을 만들지 않으며, 주입하는 쪽도 그 용도로 재사용하지 않는다).
|
|
135
|
+
|
|
136
|
+
`partnerFetchImpl`의 기본값은 `globalThis.fetch`를 쿠키 없이(`credentials: 'omit'`), 인증 헤더 없이,
|
|
137
|
+
`redirect: 'error'`로 부른다. 파트너 요청은 플랫폼 인증 fetch(`fetchImpl`)를 거치지 않는다.
|
|
138
|
+
**인증을 붙이는 `partnerFetchImpl`을 주입하는 쪽은, 파트너 백엔드가 받은 자격증명을 응답 본문에 되돌려
|
|
139
|
+
프레임으로 흘릴 수 있다는 위험을 받아들이는 것이다.** 그래서 `partnerOrigins`에는 팬 토큰을 보낼 vibe 백엔드만 넣는다.
|
|
140
|
+
|
|
141
|
+
`externalFetchImpl`의 기본값도 같은 무인증 fetch다. 토큰 없이 부르는 공개 API·CDN은 등록 없이 이 갈래로 간다.
|
|
142
|
+
external에 인증을 붙이는 fetch를 주입하지 않는다. 외부 갈래는 **옵트인**이다 — 켜면 프레임이 임의 https로 데이터를 보내고 응답을 읽는 양방향 채널이 생기므로, 패키지를 올리기만 해서 열리지 않게 기본을 `false`로 둔다. 외부 호출을 허용하는 호스트만 `allowExternal: true`를 명시한다(포털 미리보기 등은 생략).
|
|
143
|
+
|
|
144
|
+
## 요청과 이벤트
|
|
145
|
+
|
|
146
|
+
| 요청(프레임 → 호스트) | payload | 비고 |
|
|
147
|
+
| --------------------- | ----------------------------------- | ------------------------------------------------------ |
|
|
148
|
+
| `fetch` | `{ url, method?, headers?, body? }` | 위 정책 |
|
|
149
|
+
| `navigate` | `{ path }` | `/`로 시작, `//`·`/\`·공백·제어문자 거절 |
|
|
150
|
+
| `back` | `{}` | |
|
|
151
|
+
| `openExternal` | `{ url }` | https만. 처리기가 없으면 `noopener,noreferrer`로 새 창 |
|
|
152
|
+
| `toast` | `{ message, kind? }` | 500자에서 자름 |
|
|
153
|
+
| `resize` | `{ height }` | 0~20000px, rAF 디바운스 |
|
|
154
|
+
| `track` | `{ name, params? }` | 이름 100자, params 직렬화 4KB |
|
|
155
|
+
| `storage` | `{ op, key, value? }` | placement 스코프 메모리(키 50개, 값 4KB) |
|
|
156
|
+
| `log` | `{ level, message }` | real phase에서는 버림 |
|
|
157
|
+
|
|
158
|
+
이벤트(호스트 → 프레임, `host.send`): `slot.context`·`locale`(`{ locale, translations? }`)·`member`·`theme`·`visibility`.
|
|
159
|
+
|
|
160
|
+
오류 코드: `FORBIDDEN_DESTINATION`·`FORBIDDEN_METHOD`·`BODY_TOO_LARGE`·`RATE_LIMITED`·`BAD_REQUEST`·`TIMEOUT`·`NETWORK_ERROR`·`UNSUPPORTED`.
|
|
161
|
+
프레임 요청은 15초 안에 응답이 없으면 `TIMEOUT`. 핸드셰이크(`ready`)는 init을 받을 때까지 재전송한다(처음 10초는 250ms, 이후 2초 간격).
|
|
162
|
+
|
|
163
|
+
## 프레임 문서
|
|
164
|
+
|
|
165
|
+
`createSrcdoc`가 만드는 문서는 CSP로 프레임의 직접 네트워크를 좁힌다. 스크립트는 nonce가 붙은
|
|
166
|
+
부트스트랩과 `assetOrigin`의 번들만 실행된다. 부트스트랩은 브리지 클라이언트를 설치하고 init을 받은 뒤
|
|
167
|
+
엔트리를 로드해 custom element를 붙이고, 문서 높이를 `resize`로 보고한다.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
default-src 'none'
|
|
171
|
+
script-src 'nonce-…' {assetOrigin}
|
|
172
|
+
style-src 'unsafe-inline' {assetOrigin}
|
|
173
|
+
img-src {assetOrigin} {mediaOrigins…} data: blob:
|
|
174
|
+
font-src {assetOrigin} data:
|
|
175
|
+
media-src {assetOrigin} {mediaOrigins…} blob:
|
|
176
|
+
connect-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `fetch`·XHR·WebSocket·EventSource는 `connect-src 'none'`으로 막힌다. API 호출은 브리지(`request`)로만 간다 —
|
|
180
|
+
`allowExternal: true`면 브리지가 무인증 외부 https 호출을 중개하므로, 프레임이 가진 데이터는 이 경로로도 밖에 나갈 수 있다.
|
|
181
|
+
- img-src·media-src에 맨 `https:`는 두지 않는다 — 두면 `<img src="https://임의-origin/?d=…">`가 자유로운 GET
|
|
182
|
+
유출 통로가 된다. 외부 이미지·미디어가 필요하면 `mediaOrigins`에 **정확한 https origin**만 넘긴다
|
|
183
|
+
(경로·쿼리·userinfo·와일드카드·대문자 호스트 불가, 최대 20개, 하나라도 틀리면 `TypeError`). font-src는 넓히지 않는다.
|
|
184
|
+
- **CSP는 심층 방어이지 경계가 아니다.** 프레임은 자기 자신을 다른 URL로 이동(`location = …`)시킬 수 있고, 이 내비게이션은
|
|
185
|
+
CSP로 막히지 않으므로 URL에 실은 데이터는 여전히 밖으로 나갈 수 있다. 실제 경계는 **자격증명(토큰·쿠키)이 프레임에
|
|
186
|
+
절대 들어가지 않는다**는 것이다 — 인증은 호스트의 `fetchImpl`이 붙이고, 토큰을 응답으로 돌려주는 인증 경로는
|
|
187
|
+
게이트웨이 거절 목록(`gatewayDenyPrefixes`·`gatewayDenySegments`)으로 막는다. 프레임에 넘기는 init·이벤트·응답에 민감 정보를 싣지 않는다.
|
|
188
|
+
|
|
189
|
+
프레임 안 제약: 쿠키·`localStorage`·호스트 DOM에 접근할 수 없다(opaque origin). 번역 사전은 CDN이 아니라
|
|
190
|
+
호스트가 init·`locale` 이벤트의 `translations`로 넘긴다. 브리지 모드의 `fetchTranslations`는 **현재 locale 사전만**
|
|
191
|
+
돌려주고, 다른 locale(폴백 체인의 `en`·`ko` 등)은 빈 사전 `{}`이다. 같은 locale에 사전만 새로 와도 `useBstageTranslations`가 다시 읽는다.
|
|
192
|
+
|
|
193
|
+
## 개발 메모
|
|
194
|
+
|
|
195
|
+
프레임 런타임(`src/frame/runtime.ts`·`createBridgeFetch.ts`)은 `Function.prototype.toString()`으로 srcdoc에 심기는 자기완결 함수다. 바깥 식별자를 참조하거나 헬퍼가 주입되는 문법을 쓰지 않는다. 코드를 주입하는 커버리지 계측(istanbul)은 직렬화를 깨므로 v8 커버리지만 쓴다.
|