chrome-in-iframe 2.0.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,267 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ Security & robustness hardening. Runtime backwards-compatible.
6
+
7
+ ### Security
8
+
9
+ - `readProperty` denylist extended with `__defineGetter__` / `__defineSetter__` / `__lookupGetter__` / `__lookupSetter__`.
10
+ - `serialize` / `deserialize` enforce a 200-level depth cap; deeper payloads throw.
11
+ - Envelope validators cap path length (64), args count (1024), `type` (64 chars), and id fields (200 chars).
12
+ - `releaseCallbacks` now checks ID ownership against the sender — targeted callbacks can only be released by their owner.
13
+ - Incoming `postMessage` data exceeding `MAX_MESSAGE_LENGTH` (1 000 000 chars) is dropped before `JSON.parse` to guard against oversized payloads.
14
+ - `releaseCallbacks` IDs are validated as message IDs (`isMessageId`) instead of only `typeof === 'string'`.
15
+ - Auto-derived `targetOrigin` is frozen on first **successful** resolution (`freezeResolvedOrigin`). When the derivation falls back to `'*'`, the next call retries the resolution instead of permanently caching `'*'`, fixing the common "create iframe before setting `src`" pattern.
16
+ - When `targetOrigin` falls back to `'*'`, the default `allowedOrigin` no longer accepts any origin. It now tightens to `event.origin === window.location.origin`, blocking cross-origin scripts from injecting messages through the `'*'` escape hatch.
17
+ - `handleDestroyEndpoint` now validates `data.instanceId === senderInstanceId` and rejects forged destroy notifications; `isDestroyEndpoint` at the envelope layer validates `instanceId` with `isMessageId`.
18
+ - Response handlers (`invokeResponse` / `accessPropertyResponse` / `invokeFunctionByIdResponse`) now validate `meta.senderInstanceId === pending.expectedRemote`. Mismatched responses are silently dropped **without** consuming the pending entry, so the real remote's response can still land. Closes a sender-spoofing vector where another endpoint sharing the channel key could race-answer pending requests. `invoke` and `accessProperty` now also lock `expectedRemote = targetInstanceId` in `sendRequest`, matching `invokeFunctionById`.
19
+
20
+ ### Bug fixes
21
+
22
+ - `handleDestroyEndpoint` actively rejects pending requests targeted at the destroyed remote — and all broadcast pendings when the last known remote disappears — instead of letting them hang to timeout.
23
+ - `handleRemoteDestroy` scrubs `callbackOwners` of the gone remote.
24
+ - `destroy()` clears `callbackOwners` and `knownRemotes` along with the other caches.
25
+ - `destroy()` now invokes the `onReject` hook before rejecting each pending request, matching the `rejectPending` / `handleRemoteDestroy` paths. Previously persistent callbacks registered via `addListener` would leak references when the transport closed before the response arrived.
26
+ - All `onReject` / `onResolve` hooks are wrapped via `safeOnReject` / `safeInvokeResolve`: a throwing hook no longer swallows the outer `reject` / `resolve`, and the error is logged via `warn`.
27
+ - Broadcast-registered callbacks (`BROADCAST_OWNER`) now carry per-remote refcounting. A release from one remote no longer collaterally evicts the callback for other remotes still holding it — fixes the "any host can knock out everyone" failure mode when multiple hosts share a key.
28
+ - `sender.sendMessage` uses `!== undefined` for `targetInstanceId`; empty string is no longer silently treated as broadcast.
29
+ - Shared `windowDispatcher` wraps each subscriber's `deliver` in try/catch so one throwing endpoint can't affect others.
30
+ - `ERROR_FACTORIES` restores `AggregateError` and `DOMException` when available; failing to set `error.name` falls back to `Object.defineProperty`.
31
+ - Replaced the fragile `message.includes('contentWindow…')` matching with a `TRANSPORT_DETACHED` Symbol sentinel.
32
+ - `invoke` / `accessProperty` now validate the path via `serializePath` *before* creating the pending entry; a serialization failure no longer leaves a dangling pending promise.
33
+ - Response-path error rebuild upgraded from `new Error(message) + stack` to reusing the deserializer's `rebuildErrorFromSerialized`, so the original `name` (e.g. `TypeError`, `RangeError`) is preserved across the bridge.
34
+ - `serializeError` now filters own properties by `isEnumerable`, consistent with the plain-object path. Prevents subclasses / `DOMException` from leaking internal non-enumerable own props on some browsers.
35
+ - Multiple hosts sharing the same key: the client now binds to a single remote via the connect handshake and targets that remote for all subsequent calls, instead of broadcasting to every host.
36
+ - `dropLocalCallback` honors `persistentRefcount`: a remote releasing a callback id now only decrements the refcount; the local persistent entry is torn down only when refcount reaches 0. Fixes "N local `addListener(fn)` calls + one remote release wipes out all N registrations".
37
+ - The three request processors (`handleInvokeRequest`, `handleAccessPropertyRequest`, `handleCallbackInvoke`) early-return on `ctx.isDestroyed()`. Combined with reordering `endpoint.destroy()` to call `channel.destroy()` (remove the listener) before `context.destroy(true)` (drain state), eliminates post-destroy message handling that could otherwise register new callbacks into a freshly-cleared cache and leak.
38
+ - `invoke` / `invokeFunctionById` failure paths (`serialize` throw / `postMessage` throw) now release both persistent and non-persistent registered callback ids. Previously non-persistent ids lingered in `functionCache` until the 5-min LRU TTL; `invokeFunctionById` performed no cleanup at all on failure.
39
+ - `handleConnectRequest` now uses the new `noteFreshConnect`: when a previously-unseen sender announces itself, the bound remote is replaced. Fixes iframe-reload scenarios where the main-window endpoint's `boundRemoteInstanceId` stayed pinned to the old, dead instance — subsequent invokes were filtered out by channel-layer targeting and could only fail by timeout.
40
+
41
+ ### Performance
42
+
43
+ - `MAX_RELEASE_IDS` lowered to 10 000; batches over 1 000 are sliced via `queueMicrotask`.
44
+ - Multiple endpoints on the same `window` now share a single DOM `message` listener via a module-level dispatcher.
45
+ - `destroy()` de-duplicates collected `releaseCallbacks` ids with a `Set`.
46
+ - `functionIds` outer map is now a `WeakMap`, so callbacks aren't pinned after eviction.
47
+ - `releaseCallbacks` is now chunked on the *sender* side (`RELEASE_CALLBACKS_CHUNK_SIZE` = 10 000). The receiver processes each chunk at face value instead of re-splitting, reducing redundant work.
48
+ - Client proxy caches child proxies per key (`stringChildren` / `symbolChildren`). Repeated calls like `chrome.tabs.query` / `chrome.runtime.sendMessage` on the same path no longer allocate fresh `Proxy` + handler closures every time.
49
+ - Plain-object serialize / deserialize switched to direct assignment; only `__proto__` keeps `Object.defineProperty` to preserve its "data, not prototype" semantics while preventing prototype pollution. 5-10× faster on V8's plain-object hot path.
50
+ - Serializer type-dispatch order reordered: `Array.isArray` + plain-object short-circuit first (the common cases), then the `instanceof Error/Date/RegExp/Map/Set` chain.
51
+ - `callbackOwners` value type widened from `BROADCAST_OWNER | Set<string>` to `BROADCAST_OWNER | string | Set<string>`. Single-remote scenarios now store a bare string instead of a `Set`, saving ~40 bytes per callback (≈ 40 KB at 1 000 active callbacks).
52
+ - Removed the defensive `Array.from(subs)` clone in the `windowDispatcher` deliver loop; Set iterators behave correctly with add/delete on the current entry.
53
+
54
+ ### Refactor / internal
55
+
56
+ - Symbol token encode/decode (`encodeSymbolToken` / `decodeSymbolToken`) centralized in `channel/utils.ts`; `channel/path.ts`, `serializer.ts`, and `deserializer.ts` no longer keep their own copies.
57
+ - New `processor/helpers.ts` extracts `createScopedRemoteCallback`, `createGenerateCallback`, `createResponseHandler`, plus `safeInvokeReject` / `safeInvokeResolve`.
58
+ - The three response handlers (`handleAccessPropertyResponse` / `handleInvokeResponse` / `handleCallbackInvokeResponse`) collapsed onto the shared `createResponseHandler` template, eliminating three copies that had been drifting in error-handling detail.
59
+ - `setupInMainWindow` and `setupInIframe` now share an internal `setupConnection(options, sides)` helper; they differ only in target-origin resolution, postMessage destination, and `getExpectedSource`.
60
+ - `setOwnProperty` consolidated into `channel/utils.ts`; `serializer.ts` and `deserializer.ts` no longer define their own.
61
+ - `destroy()` now reuses `notifyReleaseCallbacks` for chunked release-send instead of duplicating the cursor loop.
62
+ - New `createResponder(ctx, target, responseType, id)` factory in `processor/helpers.ts` returns `{ respondError, respondThrown, respondSuccess }`, with built-in serialize-failure fallback to error response. ~70 lines of `sendXxxSuccess` / `sendXxxErrorMessage` boilerplate across the three processors collapse into direct responder calls.
63
+ - The three message-type switches in `channel/channel.ts` (`isValidMessagePayload`, `deserializeMessageData`, `sendProcessorError`) collapse onto a single `MESSAGE_SPECS: { [K in MessageType]: MessageSpec<K> }` table. Per-type validator, path pre-deserialize hook, and error-response routing live in one row; adding a new message type now means adding one entry instead of touching three switches.
64
+
65
+ ### API additions
66
+
67
+ - `ConnectionHandle.isDestroyed()` / `Endpoint.isDestroyed()`.
68
+ - `ConnectionOptions.strictOrigin` — throws on origin auto-derivation failure instead of falling back to `'*'`.
69
+ - `timeout: Infinity` disables the timeout entirely.
70
+ - Exports `TRANSPORT_DETACHED`, `isTransportDetachedError(err)`, and the `TransportDetachedError` type.
71
+
72
+ ### Internal interface widening (type-only)
73
+
74
+ - `ClientContext` gained `serializeForRemote`, `noteRemoteSeen`, `bindRemote`, `disableRemoteTargetWait`; `invokeFunctionById` and `dropLocalCallback` gained optional ownership params.
75
+ - `ClientContext.getAndRemovePendingPromise` gained an optional `senderInstanceId` parameter for sender validation.
76
+ - `ClientContext` gained `noteFreshConnect(remoteInstanceId)` for iframe-reload-aware rebinding.
77
+ - `Messages` gained `connectRequest` / `connectResponse` message types.
78
+ - `PromiseCallbacks.timer` widened to `… | null`; added optional `expectedRemote`.
79
+
80
+ ### Package metadata
81
+
82
+ - `build` script now runs `node scripts/fix-dts-extensions.mjs` after `rollup -c` to correct `.d.ts` import extensions.
83
+
84
+ ### Tests
85
+
86
+ - 20 new regression tests covering the above; suite grew 146 → 166.
87
+
88
+ ## 2.0.2
89
+
90
+ ### Package metadata
91
+
92
+ - Added a CommonJS entry at `dist/index.cjs`, exposed through `main` and
93
+ the `exports.require` condition. `require('chrome-in-iframe')` now returns
94
+ the same named runtime exports as the ESM entry.
95
+ - The CommonJS bundle resolves dependencies with browser conditions so it
96
+ does not pull in Node-only modules such as `node:crypto` when bundled for
97
+ Chrome extension pages or iframes.
98
+ - `exports.default` now points to the ESM entry so bundlers that fall
99
+ through to `default` get the browser-friendly bundle.
100
+
101
+ ## 2.0.1
102
+
103
+ ### Bug fixes
104
+
105
+ - `handleReleaseCallbacks` now also evicts the corresponding entries from the
106
+ receiver's `remoteCallbacksByOwner` / `persistentRemoteCallbacksByOwner`
107
+ maps. Previously only the receiver's own `functionCache` /
108
+ `persistentFunctionCache` were touched, so the host kept the wrapper
109
+ function for every `chrome.*.onX.addListener(handler)` indefinitely — a
110
+ real leak in long-lived service workers with frequent listener churn.
111
+ - Reordered the `removeListener` flow on the sender side: the
112
+ `invokeRequest` is now dispatched before the `releaseCallbacks` so the
113
+ receiver can still resolve the cached wrapper, call
114
+ `chrome.*.removeListener(W)` correctly, and only afterwards drop it.
115
+ - The error response handlers (`invokeResponse`, `accessPropertyResponse`,
116
+ `invokeFunctionByIdResponse`) no longer clobber the local Error's stack
117
+ with `undefined` when the remote payload has no `stack` field.
118
+ - A function reused as a listener for several events (e.g. both
119
+ `tabs.onActivated.addListener(handler)` and
120
+ `tabs.onUpdated.addListener(handler)`) is now reference-counted on the
121
+ sender side. Removing one registration no longer evicts the shared id, so
122
+ the other listener keeps working.
123
+ - `handleInvokeRequest` rejects empty-path invocations with a clear error
124
+ instead of bottoming out at `'undefined' is not a function`.
125
+ - Mid-path read errors now report the actual parent value (`null` /
126
+ `undefined`) plus the full traversed prefix, instead of stringifying the
127
+ previous path segment.
128
+ - `isAllowedOrigin` distinguishes `undefined` (no restriction) from `''`
129
+ (empty allow-list); the empty string is no longer treated as
130
+ "allow everything".
131
+ - When `allowedOrigin` is omitted, incoming `postMessage` events are now
132
+ filtered against the same concrete origin resolved for `targetOrigin`.
133
+ This keeps the default bridge configuration from accepting messages
134
+ from an unexpected origin while still preserving the documented `'*'`
135
+ fallback when origin auto-detection cannot resolve one.
136
+ - `invoke` / `accessProperty` / `invokeFunctionById` now clear the pending
137
+ Map entry and timer when `serialize` / `sendMessage` throws synchronously.
138
+ Previously the entry lingered until the timeout fired.
139
+ - `timeout` is clamped to the default when given a non-finite or
140
+ non-positive value, so accidental `timeout: 0` no longer rejects every
141
+ call instantly.
142
+ - Listener registration/removal is now transactional. Failed
143
+ `addListener` calls roll back local persistent callback state and ask the
144
+ peer to release any wrapper it created; failed `removeListener` calls no
145
+ longer invalidate a callback that the remote side still has registered.
146
+ - Remote method and callback results now treat any thenable as async instead
147
+ of relying on `instanceof Promise`, so cross-realm Promises and custom
148
+ thenables resolve/reject correctly.
149
+ - `destroy()` no longer warns when the only failure is an already-detached
150
+ iframe whose `contentWindow` is unavailable during shutdown.
151
+ - Nested function arguments inside `addListener` payloads are now
152
+ release-tracked. Previously only top-level function arguments were
153
+ collected for cleanup, so a call like
154
+ `addListener({ filter, handler })` left `handler` pinned in the
155
+ persistent cache even after the matching `removeListener`.
156
+ - The `setTimeout` branch in `invoke` / `accessProperty` /
157
+ `invokeFunctionById` now triggers the pending entry's `onReject`
158
+ before rejecting. A timed-out `addListener` no longer leaks its
159
+ persistent registration.
160
+ - `hasListener` is no longer classified as a listener-registration path.
161
+ Its argument used to be pinned as persistent indefinitely, because
162
+ `hasListener` has no `removeListener` counterpart that would refcount
163
+ it back down.
164
+ - Sender-side function ids are now keyed by `(fn, thisArg)` rather than
165
+ just `fn`. The same function reused under different owners (e.g. one
166
+ method shared across two parent objects) no longer overwrites the
167
+ first registration's `thisArg`.
168
+ - `accessPropertyRequest` now rejects intermediate-`null` /
169
+ `undefined` traversal with the same diagnostic format as
170
+ `invokeRequest` (`Cannot read property 'X' of null (at 'a.b')`),
171
+ instead of silently returning the partial value. Leaf
172
+ `null` / `undefined` still resolves normally.
173
+ - Endpoint `destroy(notifyRemote=true)` now sends a `releaseCallbacks`
174
+ for every remote callback this endpoint is still holding before
175
+ emitting `destroyEndpoint`. This evicts the peer's persistent-flagged
176
+ methods that paths like `$get('runtime.onMessage')` had pinned on its
177
+ side — fixing a leak in long-lived hosts whose clients open and close.
178
+ - `handleReleaseCallbacks` now fully cleans `functionIds` and
179
+ `persistentRefcount` via the new `ctx.dropLocalCallback(id)`, not just
180
+ the two outer caches.
181
+ - `accessPropertyRequest` / `invokeRequest` no-delegate-target checks
182
+ now use explicit `=== undefined || === null` instead of `!target`, so
183
+ a delegate that is `0` / `false` (e.g. a numeric-rooted custom RPC
184
+ target) is no longer misreported as missing.
185
+ - `isMessageEnvelope` rejects messages whose `senderInstanceId` or
186
+ `type` is the empty string, in addition to the previous non-string
187
+ check.
188
+ - Error payload extras are now restored with own-property definitions
189
+ rather than direct assignment, so fields such as `__proto__` remain data
190
+ and cannot mutate the deserialized Error object's prototype.
191
+
192
+ ### Performance / resource
193
+
194
+ - The `message` listener now does a cheap pre-filter (string check + `'{'`
195
+ start + `"key":<jsonStringifyOfKey>` substring) before `JSON.parse`, so
196
+ unrelated `window.postMessage` traffic from other libraries is rejected
197
+ without parsing. The match string handles JSON-escaped key values, so
198
+ keys containing `"` or `\` route correctly.
199
+ - Well-known symbol reverse lookup is now O(1) via a `Map<symbol, string>`
200
+ built once at module load.
201
+ - The deep payload validators run only after `handler` lookup succeeds; the
202
+ cheap envelope check (type/key/senderInstanceId shape) runs first.
203
+ - `createWindowPoster.addEventListener` is idempotent for the same callback:
204
+ re-registering with the same listener instance unregisters the previous
205
+ DOM listener first, preventing accidental window-listener leaks.
206
+ - `handleReleaseCallbacks` caps the batch at 100 000 ids, warning and
207
+ truncating instead of silently dropping the whole batch on overflow.
208
+ - `endpoint.destroy()` is idempotent — extra calls are no-ops, so an
209
+ accidental double-destroy from cleanup hooks no longer pokes the
210
+ underlying poster twice.
211
+ - `createWindowPoster`'s listener map is keyed by `(name, callback)`
212
+ rather than just `callback`. The same callback registered against
213
+ different event names no longer overwrites each other.
214
+ - `bindMethod`'s WeakMap cache is now owned by each `ClientContext`
215
+ rather than being module-global. Multiple endpoints in the same
216
+ process no longer share bound-method state.
217
+
218
+ ### API
219
+
220
+ - New: `setLogger(logger | null)` and the `Logger` type are exported from
221
+ the package entry. Pass `null` to silence all library warnings, or pass
222
+ your own function to route them through a custom sink (e.g. Sentry).
223
+
224
+ ### Removed (internal type surface)
225
+
226
+ - The following members were removed from interfaces that are re-exported
227
+ for advanced use. They had no internal or external callers; removing them
228
+ is technically a type-level breaking change for anyone who imported and
229
+ asserted against these shapes, but the runtime behavior is unchanged:
230
+ - `ClientContext.registerPendingPromise`
231
+ - `MessageChannel.getContext`, `getPoster`, `getKey`, `getInstanceId`
232
+
233
+ ### Logging
234
+
235
+ - Extracted a unified `warn()` utility (`src/log.ts`) with a `[chrome-in-iframe]` prefix and scope tag for all internal warnings.
236
+ - Replaced all silent `catch` blocks (comment-only / bare `return`) with `warn()` calls so previously swallowed errors are now visible in the console. Affected areas: origin derivation (`deriveOriginFromIframe`, `deriveParentOrigin`, `parseOrigin`), message dispatch (`createMessageChannel`), deserialization (`deserializeError`, `deserializeWrappedObject`, `resolveObjectKey`), and client lifecycle (`notifyReleaseCallbacks`, `destroy`).
237
+ - Migrated existing ad-hoc `console.warn` calls to use the centralized `warn()` utility.
238
+
239
+ ### Package metadata
240
+
241
+ - `exports` map now exposes top-level `types` / `import` / `default`
242
+ conditions and `"./package.json"` for tooling that wants to read the
243
+ manifest. Fixes resolution under stricter bundlers (webpack 5 strict
244
+ exports) without changing the canonical entry.
245
+ - Removed the `clean` and `test:watch` scripts. `build` now runs `rollup -c`
246
+ directly without a preceding `clean` step.
247
+
248
+ ### Tests
249
+
250
+ - New `test/chrome-api.test.ts` with 53 tests covering all 12 Chrome MV3 API
251
+ patterns through the bridge: Promise async methods (various return types),
252
+ synchronous methods proxied as async, Port objects, event listeners
253
+ (addListener/removeListener/hasListener, multi-arg callbacks, sendResponse),
254
+ nested namespaces, optional leading parameters, static properties via `$get`,
255
+ complex data round-trips, CRUD registration, multi-client event isolation,
256
+ error propagation, and timeout behavior.
257
+ - 8 new regression tests in `test/bridge.test.ts` covering each of the
258
+ newly fixed defects: nested-callback release on `removeListener`,
259
+ persistent rollback on `addListener` timeout, non-persistent
260
+ `hasListener` argument, mid-path null/undefined diagnostics through
261
+ `$get`, distinct `(fn, thisArg)` entries for the same function, host
262
+ persistent-cache cleanup on client `destroy()`, idempotent
263
+ `endpoint.destroy()`, and empty-`senderInstanceId` envelope rejection.
264
+
3
265
  ## 2.0.0
4
266
 
5
267
  Breaking changes — both peers of the bridge must run the same major version.
@@ -1,5 +1,233 @@
1
1
  # 更新日志
2
2
 
3
+ ## 2.1.0
4
+
5
+ 安全与稳健性加固。运行时完全向后兼容。
6
+
7
+ ### 安全
8
+
9
+ - `readProperty` 危险属性黑名单扩充 `__defineGetter__` / `__defineSetter__` / `__lookupGetter__` / `__lookupSetter__`。
10
+ - `serialize` / `deserialize` 加入 200 层深度上限,超出抛 `TypeError`。
11
+ - envelope 校验加入路径段数(64)、参数数量(1024)、type 长度(64)、id 长度(200)上限。
12
+ - `releaseCallbacks` 按 sender 校验 ID 归属 —— targeted callback 只允许 owner remote 释放。
13
+ - 传入的 `postMessage` 数据超过 `MAX_MESSAGE_LENGTH`(1 000 000 字符)时在 `JSON.parse` 前直接丢弃,防御超大载荷。
14
+ - `releaseCallbacks` 的 ID 字段现在使用 `isMessageId` 校验,不再仅做 `typeof === 'string'` 检查。
15
+ - 自动推导的 `targetOrigin` 首次成功解析后被冻结(`freezeResolvedOrigin`);若解析回退到 `'*'`,下次调用会重试,避免「iframe 创建早于 `src` 赋值」的常见模式被永久卡在 `'*'`。
16
+ - 当 `targetOrigin` 已经回退到 `'*'` 时,自动派生的 `allowedOrigin` 不再退化为「接受任意 origin」,而是收紧为 `event.origin === window.location.origin`,阻止跨域脚本经由 `'*'` 注入消息。
17
+ - `handleDestroyEndpoint` 现在校验 `data.instanceId === senderInstanceId`,拒绝伪造的销毁指令;`isDestroyEndpoint` 在 envelope 层使用 `isMessageId` 校验 `instanceId`。
18
+ - 响应处理器(`invokeResponse` / `accessPropertyResponse` / `invokeFunctionByIdResponse`)现在校验 `meta.senderInstanceId === pending.expectedRemote`:不匹配的响应会被静默丢弃且**不消费** pending 条目,真实远端的响应仍能继续命中。封堵了「共享同一 key 的其他 endpoint 伪造响应抢答」这一攻击面。`invoke` 与 `accessProperty` 现在也会在 `sendRequest` 中锁定 `expectedRemote = targetInstanceId`,与 `invokeFunctionById` 行为一致。
19
+
20
+ ### Bug 修复
21
+
22
+ - `handleDestroyEndpoint` 主动 reject 本端 pending(等该 remote 响应的;以及最后一个 known remote 消失时所有 broadcast pending),不再卡到 timeout。
23
+ - `handleRemoteDestroy` 同时清理 `callbackOwners` 中该 remote 的痕迹。
24
+ - `destroy()` 一并清理 `callbackOwners` 和 `knownRemotes`。
25
+ - `destroy()` reject 每个 pending 之前现在会调用 `onReject` 钩子,与 `rejectPending` / `handleRemoteDestroy` 路径一致;此前 addListener 路径上的 persistent callback 在 transport 提前关闭时不会被释放。
26
+ - 所有 `onReject` / `onResolve` 钩子统一通过 `safeOnReject` / `safeInvokeResolve` 包装:钩子内部抛错不再吞掉外层 `reject`/`resolve`,错误以 `warn` 记录。
27
+ - broadcast 注册的 callback(`BROADCAST_OWNER`)现在带 per-remote 引用计数:一个 remote 释放该 ID 不再连带清除其它 remote 仍持有的引用,避免「多 host 共用 key 时,任一 host 单方释放就让全员失效」。
28
+ - `sender.sendMessage` 中 `targetInstanceId` 改用 `!== undefined`,避免空字符串被当 broadcast。
29
+ - 共享 `windowDispatcher` 对订阅者的 `deliver` 加 try/catch 隔离,单个订阅者抛错不影响其它。
30
+ - `ERROR_FACTORIES` 增补 `AggregateError` 和 `DOMException`;`error.name` 写入失败用 `Object.defineProperty` 兜底。
31
+ - 用 `TRANSPORT_DETACHED` Symbol sentinel 取代脆弱的 `message.includes('contentWindow…')` 字符串匹配。
32
+ - `invoke` / `accessProperty` 在创建 pending 条目前先通过 `serializePath` 校验路径;序列化失败不再残留悬挂的 pending promise。
33
+ - response 路径的错误重建从「`new Error(message) + stack`」升级为复用 `deserialize` 的 `rebuildErrorFromSerialized`,保留 `name`(`TypeError` / `RangeError` 等)。
34
+ - `serializeError` 现在按 `isEnumerable` 过滤 own 属性,与 plain-object 序列化保持一致,避免子类 / DOMException 在某些浏览器上把内部 non-enumerable own props 也带出去。
35
+ - 多个 host 共享同一 key 时,client 通过 connect 握手绑定单一 remote,后续所有调用精确路由到该 remote,不再广播给所有 host。
36
+ - `dropLocalCallback` 现在会先递减 `persistentRefcount`,归零后才真正销毁 `persistentFunctionCache` 条目。修复「本地多次 `addListener(fn)` 后,远端任一 release 让所有注册同时失效」的问题。
37
+ - 三个 request processor(`handleInvokeRequest` / `handleAccessPropertyRequest` / `handleCallbackInvoke`)在入口处补上 `ctx.isDestroyed()` 守卫;同时 `endpoint.destroy()` 的顺序调整为「先 `channel.destroy()` 摘掉 listener,再 `context.destroy(true)` 清空状态」。两者配合后,destroy 之后到达的消息会被直接忽略,不会再把新 callback 注册进已清空的 cache 造成泄漏。
38
+ - `invoke` / `invokeFunctionById` 的失败路径(`serialize` 抛错 / `postMessage` 抛错)现在会同时释放 persistent 与非 persistent 的已注册 callback。此前非 persistent 的 id 会一直占着 `functionCache`,要等到 LRU TTL(5 min)才回收;`invokeFunctionById` 失败时则根本不做清理。
39
+ - `handleConnectRequest` 改用新增的 `noteFreshConnect`:当一个从未见过的 sender 发来 `connectRequest` 时直接替换 `boundRemoteInstanceId`。修复 iframe reload 场景——主窗口 endpoint 不会再继续把 invoke 发往已失效的旧 instanceId,只能干等到超时。
40
+
41
+ ### 性能
42
+
43
+ - `MAX_RELEASE_IDS` 降到 10 000;超过 1 000 条改用 `queueMicrotask` 切片处理。
44
+ - 同一 `window` 上多 endpoint 共享一个 DOM `message` listener(module-level dispatcher)。
45
+ - `destroy()` 收集 release id 用 `Set` 去重。
46
+ - `functionIds` 外层改用 `WeakMap`,callback 不再被强引用。
47
+ - `releaseCallbacks` 在发送端按 `RELEASE_CALLBACKS_CHUNK_SIZE`(10 000)切片发送,接收端直接处理各分片,减少重复拆分工作。
48
+ - client proxy 子节点按 key 缓存(`stringChildren` / `symbolChildren`):重复调用 `chrome.tabs.query` / `chrome.runtime.sendMessage` 等同一路径不再每次新建 `Proxy` + handler 闭包。
49
+ - plain-object 序列化 / 反序列化改用直接赋值,仅对 `__proto__` 这一危险 key 保留 `Object.defineProperty`(避免原型污染同时不丢失 `__proto__` 作为 own data 的语义)。在 V8 上 plain-object 分支提速 5-10 倍。
50
+ - `serializer` 类型派发顺序优化:先 `Array.isArray` + plain-object 短路(最常见情形),再走 `instanceof Error/Date/RegExp/Map/Set` 链。
51
+ - `callbackOwners` value 由「`BROADCAST_OWNER | Set<string>`」改为「`BROADCAST_OWNER | string | Set<string>`」三态:单 remote 场景只存字符串引用,省去 `Set` 实例(每条节省 ~40 字节,1000 个活跃 callback ≈ 40KB)。
52
+ - 移除 `windowDispatcher` 派发循环中的防御性 `Array.from(subs)` 拷贝;Set 迭代器对 add/delete 当前项行为良好。
53
+
54
+ ### 重构 / 内部
55
+
56
+ - 符号 token 编解码(`encodeSymbolToken` / `decodeSymbolToken`)集中到 `channel/utils.ts`;`channel/path.ts`、`serializer.ts`、`deserializer.ts` 不再各自维护一份。
57
+ - 新增 `processor/helpers.ts`:抽出 `createScopedRemoteCallback`、`createGenerateCallback`、`createResponseHandler` 模板,以及 `safeInvokeReject` / `safeInvokeResolve`。
58
+ - 三个 response handler(`handleAccessPropertyResponse` / `handleInvokeResponse` / `handleCallbackInvokeResponse`)合并为 `createResponseHandler` 同一模板,避免三份拷贝独立演化。
59
+ - `setupInMainWindow` 与 `setupInIframe` 共享内部 `setupConnection(options, sides)` helper,二者只在「目标 origin 解析、postMessage 目标、expectedSource」三处不同。
60
+ - `setOwnProperty` 单一来源(`channel/utils.ts`),`serializer.ts` 与 `deserializer.ts` 不再各自定义。
61
+ - `destroy()` 复用 `notifyReleaseCallbacks` 完成分片发送,去掉了那段重复的 cursor 循环。
62
+ - 新增 `createResponder(ctx, target, responseType, id)` 工厂(`processor/helpers.ts`),返回 `{ respondError, respondThrown, respondSuccess }`,并内置「序列化失败时自动降级为 error 响应」的兜底。三个 processor 里近 70 行 `sendXxxSuccess` / `sendXxxErrorMessage` 模板全部改为直接调用 responder。
63
+ - `channel/channel.ts` 中原本散落的三个 switch(`isValidMessagePayload` / `deserializeMessageData` / `sendProcessorError`)合并为一张 `MESSAGE_SPECS: { [K in MessageType]: MessageSpec<K> }` 表:每条消息的 validator、path 预反序列化、错误响应路由都集中在一处。今后新增消息类型只需添加一行 spec entry,不必再同步三处分支。
64
+
65
+ ### API 新增
66
+
67
+ - `ConnectionHandle.isDestroyed()` / `Endpoint.isDestroyed()`。
68
+ - `ConnectionOptions.strictOrigin` —— origin 推导失败时抛错而非降级到 `'*'`。
69
+ - `timeout: Infinity` 表示禁用超时。
70
+ - 导出 `TRANSPORT_DETACHED` symbol、`isTransportDetachedError(err)` 类型守卫、`TransportDetachedError` 类型。
71
+
72
+ ### 内部接口扩展(仅类型层面)
73
+
74
+ - `ClientContext` 新增 `serializeForRemote`、`noteRemoteSeen`、`bindRemote`、`disableRemoteTargetWait`;`invokeFunctionById` / `dropLocalCallback` 增加可选 owner 参数。
75
+ - `ClientContext.getAndRemovePendingPromise` 新增可选的 `senderInstanceId` 参数,用于 sender 校验。
76
+ - `ClientContext` 新增 `noteFreshConnect(remoteInstanceId)`,用于在 iframe reload 场景下替换已陈旧的 `boundRemoteInstanceId`。
77
+ - `Messages` 新增 `connectRequest` / `connectResponse` 消息类型。
78
+ - `PromiseCallbacks.timer` 类型放宽为 `… | null`;新增可选 `expectedRemote`。
79
+
80
+ ### 打包配置
81
+
82
+ - `build` 脚本在 `rollup -c` 之后新增 `node scripts/fix-dts-extensions.mjs` 步骤,修正 `.d.ts` 文件中的 import 扩展名。
83
+
84
+ ### 测试
85
+
86
+ - 新增 20 条针对上述修复的回归测试;总数 146 → 166,全部通过。
87
+
88
+ ## 2.0.2
89
+
90
+ ### 打包配置
91
+
92
+ - 新增 CommonJS 入口 `dist/index.cjs`,并通过 `main` 和 `exports.require`
93
+ 暴露。`require('chrome-in-iframe')` 现在会返回与 ESM 入口一致的运行时
94
+ 命名导出。
95
+ - CommonJS bundle 会按浏览器条件解析依赖,因此打包到 Chrome 扩展页面或
96
+ iframe 时不会引入 `node:crypto` 等仅 Node 可用的模块。
97
+ - `exports.default` 现在指向 ESM 入口,确保回退到 `default` 条件的
98
+ 打包器拿到浏览器友好的产物。
99
+
100
+ ## 2.0.1
101
+
102
+ ### Bug 修复
103
+
104
+ - `handleReleaseCallbacks` 现在会同时清理接收方 `remoteCallbacksByOwner` /
105
+ `persistentRemoteCallbacksByOwner` 中对应的条目。此前只清理接收方自身
106
+ 的 `functionCache` / `persistentFunctionCache`,导致每次
107
+ `chrome.*.onX.addListener(handler)` 在宿主端留下的包裹函数永远不会被
108
+ 释放 —— 在长期运行的 service worker 中频繁增删监听器会造成真实泄漏。
109
+ - 调整 `removeListener` 的客户端发送顺序:先发送 `invokeRequest`,再发送
110
+ `releaseCallbacks`。这样接收方可以先用缓存中的包裹函数调用
111
+ `chrome.*.removeListener(W)`,然后再清理缓存。
112
+ - 错误响应处理(`invokeResponse` / `accessPropertyResponse` /
113
+ `invokeFunctionByIdResponse`)不再在远端 payload 没有 `stack` 字段时
114
+ 用 `undefined` 覆盖本地 `Error` 的 stack。
115
+ - 同一个函数被注册为多个事件的监听器(如同时绑定
116
+ `tabs.onActivated.addListener(handler)` 和
117
+ `tabs.onUpdated.addListener(handler)`)时,客户端现在按 callback id
118
+ 做引用计数。移除其中一处不再让另一处失效。
119
+ - `handleInvokeRequest` 在 path 为空时直接返回明确的错误,而不是落到
120
+ `'undefined' is not a function`。
121
+ - 中间路径解到 null / undefined 时的错误信息现在准确报告父值类型,并附
122
+ 上已访问的路径前缀。
123
+ - `isAllowedOrigin` 区分 `undefined`(不限制)与 `''`(空白名单)。空字符串
124
+ 不再被当作「放行所有 origin」。
125
+ - 未传 `allowedOrigin` 时,传入的 `postMessage` 现在会默认按
126
+ `targetOrigin` 推导出的同一个具体 origin 过滤。这样默认桥接配置不会
127
+ 接收来自意外 origin 的消息,同时仍保留自动推导失败时回退到 `'*'` 的
128
+ 既有行为。
129
+ - `invoke` / `accessProperty` / `invokeFunctionById` 在 `serialize` /
130
+ `sendMessage` 同步抛错时会清掉 pending 条目和 timer,不再等到 timeout
131
+ 才回收。
132
+ - 非有限或非正的 `timeout` 值会回落到默认值,避免 `timeout: 0` 导致每
133
+ 次调用瞬间被拒。
134
+ - 监听器注册/移除现在是事务式的。失败的 `addListener` 会回滚本地持久
135
+ callback 状态,并通知对端释放已创建的包裹函数;失败的
136
+ `removeListener` 不再让仍注册在远端的 callback 失效。
137
+ - 远端方法和 callback 返回值现在按 thenable 判断异步,不再依赖
138
+ `instanceof Promise`,因此跨 realm Promise 和自定义 thenable 可以正确
139
+ resolve/reject。
140
+ - `destroy()` 在关闭阶段如果只是因为 iframe 已分离、`contentWindow`
141
+ 不可用而通知失败,不再输出 warning。
142
+ - `addListener` 参数中嵌套对象内的回调函数现在也参与释放计数。此前
143
+ 只有顶层函数参数会被收集到回滚清单中,所以
144
+ `addListener({ filter, handler })` 这种调用即使后续 `removeListener`,
145
+ `handler` 也会一直留在持久缓存里。
146
+ - `invoke` / `accessProperty` / `invokeFunctionById` 的 `setTimeout`
147
+ 分支现在会在 reject 之前触发挂起项的 `onReject`。`addListener`
148
+ 调用超时不再造成持久注册泄漏。
149
+ - `hasListener` 不再被识别为监听器注册路径。其参数此前会被永久标记
150
+ 为 persistent —— 因为 `hasListener` 没有对应的 `removeListener`
151
+ 可以把引用计数减回去。
152
+ - 客户端的函数 id 现在以 `(fn, thisArg)` 为联合键,而不是仅以 `fn`
153
+ 为键。同一函数被复用到不同 owner 上(例如同一方法挂在两个父对象
154
+ 上)不再覆盖前一次注册的 `thisArg`。
155
+ - `accessPropertyRequest` 中间节点解到 null / undefined 时,现在
156
+ 和 `invokeRequest` 一样抛出 `Cannot read property 'X' of null
157
+ (at 'a.b')` 的诊断错误,不再静默返回中间值。叶子节点为
158
+ null / undefined 仍正常返回。
159
+ - 端点 `destroy(notifyRemote=true)` 现在会在发送 `destroyEndpoint`
160
+ 之前,先发送一条 `releaseCallbacks`,列出本端当前持有的所有远端
161
+ callback id。这样对端就可以释放被 `$get('runtime.onMessage')`
162
+ 这类路径标记为 persistent 的方法 —— 修复了长期运行宿主里客户端
163
+ 反复 open/close 时的累积泄漏。
164
+ - `handleReleaseCallbacks` 现在通过新增的 `ctx.dropLocalCallback(id)`
165
+ 彻底清理 `functionIds` 和 `persistentRefcount`,而不是只清理外层
166
+ 两个缓存。
167
+ - `accessPropertyRequest` / `invokeRequest` 的「无 delegate target」
168
+ 判断改为显式 `=== undefined || === null`,不再用 `!target`。这样
169
+ `0` / `false`(例如以数字为根的自定义 RPC target)不会被误报为
170
+ 「未配置 delegate target」。
171
+ - `isMessageEnvelope` 在原有的非字符串校验之外,还会拒绝
172
+ `senderInstanceId` / `type` 为空字符串的消息。
173
+ - Error payload 的额外字段现在通过 own-property 定义恢复,而不是直接赋值。
174
+ 因此 `__proto__` 等字段会被保留为普通数据,不会改变反序列化后 Error
175
+ 对象的原型。
176
+
177
+ ### 性能 / 资源
178
+
179
+ - `message` 监听器在 `JSON.parse` 之前先做廉价过滤(字符串检查 + `'{'`
180
+ 起始 + `"key":<JSON.stringify(key)>` 子串)。其它库通过
181
+ `window.postMessage` 发出的无关消息不再被解析。匹配串经过 JSON 转义,
182
+ 含 `"` 或 `\` 的 key 也能正确路由。
183
+ - 知名 Symbol 反向查找改为模块加载时一次性建立的 `Map<symbol, string>`,
184
+ O(1) 替代之前的 O(n)。
185
+ - envelope(type / key / senderInstanceId 形状)的廉价校验先做;深层
186
+ payload 校验延后到 handler 命中之后再执行。
187
+ - `createWindowPoster.addEventListener` 对同一 callback 幂等:再次注册
188
+ 会先卸掉旧的 DOM listener,避免 window-listener 泄漏。
189
+ - `handleReleaseCallbacks` 将单批上限改为 100 000 个 id,超过时截断
190
+ 并输出警告,而不是直接丢弃整批。
191
+ - `endpoint.destroy()` 改为幂等——重复调用直接 no-op,避免清理钩子
192
+ 里不小心的 double-destroy 让底层 poster 被处理两次。
193
+ - `createWindowPoster` 的 listener map 改为以 `(name, callback)` 为
194
+ 联合键,不再仅以 `callback` 为键。同一 callback 注册到不同事件名
195
+ 时不会再相互覆盖。
196
+ - `bindMethod` 的 WeakMap 缓存改由每个 `ClientContext` 各自持有,
197
+ 不再是模块级全局。同一进程中多个端点不再共享绑定方法状态。
198
+
199
+ ### API
200
+
201
+ - 新增:从包入口导出 `setLogger(logger | null)` 和 `Logger` 类型。传
202
+ `null` 可彻底静默库内警告,也可传入自定义函数把日志路由到 Sentry 等
203
+ 外部 sink。
204
+
205
+ ### 移除(内部类型层面)
206
+
207
+ - 以下接口成员被移除。它们在源码和测试中都没有调用方,从未被实际使
208
+ 用;运行时行为完全不变,仅在严格断言这些接口形状的下游代码看来属
209
+ 于类型层面的破坏性变更:
210
+ - `ClientContext.registerPendingPromise`
211
+ - `MessageChannel.getContext` / `getPoster` / `getKey` / `getInstanceId`
212
+
213
+ ### 日志
214
+
215
+ - 提取统一的 `warn()` 工具函数(`src/log.ts`),所有内部警告均带有 `[chrome-in-iframe]` 前缀和 scope 标签。
216
+ - 将所有静默 `catch` 块(仅注释 / 空 `return`)替换为 `warn()` 调用,此前被吞掉的错误现在会在控制台输出。涉及区域:源推导(`deriveOriginFromIframe`、`deriveParentOrigin`、`parseOrigin`)、消息分发(`createMessageChannel`)、反序列化(`deserializeError`、`deserializeWrappedObject`、`resolveObjectKey`)以及客户端生命周期(`notifyReleaseCallbacks`、`destroy`)。
217
+ - 将现有的裸 `console.warn` 调用迁移至集中的 `warn()` 工具。
218
+
219
+ ### 打包配置
220
+
221
+ - `exports` map 新增顶层 `types` / `import` / `default` 条件以及
222
+ `"./package.json"` 子路径,修复 webpack 5 严格 exports 等工具链下的
223
+ 解析问题。规范入口未变。
224
+ - 移除 `clean` 和 `test:watch` 脚本,`build` 直接运行 `rollup -c`,不再前置清理步骤。
225
+
226
+ ### 测试
227
+
228
+ - 新增 `test/chrome-api.test.ts`,包含 53 个测试用例,覆盖全部 12 种 Chrome MV3 API 调用模式的桥接测试:Promise 异步方法(各种返回类型)、同步方法代理、Port 对象、事件监听器(addListener/removeListener/hasListener、多参数回调、sendResponse)、嵌套命名空间、可选前导参数、$get 静态属性、复杂数据往返、CRUD 注册模式、多客户端事件隔离、错误传播、超时行为。
229
+ - 在 `test/bridge.test.ts` 末尾新增 8 个针对上述新修复点的回归测试:`removeListener` 时嵌套回调被释放、`addListener` 超时回滚持久注册、`hasListener` 参数不被标记为 persistent、通过 `$get` 时中间路径 null/undefined 的诊断错误、同一函数不同 owner 产生不同 entry、客户端 `destroy()` 触发宿主端持久缓存清零、`endpoint.destroy()` 幂等、空 `senderInstanceId` 信封被拒收。
230
+
3
231
  ## 2.0.0
4
232
 
5
233
  破坏性变更 — 桥接的两端必须运行相同的主版本号。
@@ -8,7 +236,7 @@
8
236
 
9
237
  - 将魔法字符串前缀(`$undefined$`、`$function$id` 等)替换为对象标签(`{ $cii$: 'undef' }`、`{ $cii$: 'fn', id }` 等)。与保留标记冲突的普通字符串现在可以正确地往返传输。
10
238
  - 新增对 `Date`、`RegExp`、`Map`、`Set`、`BigInt`、`NaN`、`Infinity`、`-Infinity` 和 `-0` 的序列化支持。
11
- - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有品牌 Symbol 的用户对象不再导致序列化抛出异常。
239
+ - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有 brand Symbol 的用户对象不再导致序列化抛出异常。
12
240
  - `Error` 序列化保留 `name`(因此 `TypeError` 传输后仍为 `TypeError`)、`cause`(递归)、`stack` 以及额外的自有可枚举字符串键属性(如自定义 `code` 字段)。
13
241
  - 标签载荷会进行校验;格式错误的 `date` / `regexp` / `bigint` / `fn` 载荷会抛出带有描述信息的 `TypeError`,而不是产生 `Invalid Date` 或静默返回 undefined。当条目包含无法解析的键时,`deserializeWrappedObject` 会向控制台输出警告,而非静默丢弃。
14
242
 
@@ -54,7 +282,7 @@
54
282
 
55
283
  - 本包**仅支持 ESM**。没有 CommonJS 入口。`require('chrome-in-iframe')` 将无法工作 — 请在 ESM 项目中使用或配置打包器将其作为 ESM 外部依赖处理。
56
284
 
57
- ### 包元数据
285
+ ### 打包配置
58
286
 
59
287
  - 添加了 `engines`、`sideEffects: false`。
60
288
  - `prepublishOnly` 依次运行 `typecheck`、`test` 和 `build`。
package/README.md CHANGED
@@ -10,17 +10,17 @@ This is useful when an extension page such as `options.html`, `popup.html`, or a
10
10
 
11
11
  ## Module Format
12
12
 
13
- **This package is ESM-only.** It ships a single ECMAScript module and exposes no CommonJS entry point. Use it from an ESM project (Vite, Rollup, webpack 5+, Parcel 2+, modern bundlers, or native `<script type="module">`).
13
+ This package ships both ESM and CommonJS entry points. Modern bundlers and native module code should use the ESM entry automatically; CommonJS consumers can use `require()`.
14
14
 
15
- `require('chrome-in-iframe')` will not work.
15
+ ```ts
16
+ import { connectChromeInIframe, exposeChromeInIframe } from 'chrome-in-iframe';
17
+ ```
16
18
 
17
- ```json
18
- {
19
- "type": "module"
20
- }
19
+ ```js
20
+ const { connectChromeInIframe, exposeChromeInIframe } = require('chrome-in-iframe');
21
21
  ```
22
22
 
23
- If your tooling still emits CommonJS, configure it to leave `chrome-in-iframe` as an ESM external, or migrate the consuming project to ESM.
23
+ The CommonJS build is generated with browser-oriented dependency resolution so it can still be bundled for Chrome extension pages and iframes.
24
24
 
25
25
  ## Install
26
26
 
@@ -147,9 +147,11 @@ window.addEventListener('beforeunload', () => {
147
147
 
148
148
  ## Security Options
149
149
 
150
- `targetOrigin` and `allowedOrigin` are optional. When `targetOrigin` is not supplied, the library resolves it from `iframe.src` / `contentWindow.location.origin` (on the extension side) or from `document.referrer` / `location.ancestorOrigins` (on the iframe side); it only falls back to `'*'` when none of those are available. Incoming messages are additionally checked against the expected window source.
150
+ `targetOrigin` and `allowedOrigin` are optional. When `targetOrigin` is not supplied, the library resolves it from `iframe.src` / `contentWindow.location.origin` (on the extension side) or from `document.referrer` / `location.ancestorOrigins` (on the iframe side); it only falls back to `'*'` when none of those are available. Incoming messages are checked against both the expected window source and, by default, the same origin resolved for `targetOrigin`. When `targetOrigin` falls back to `'*'`, the default `allowedOrigin` tightens to `event.origin === window.location.origin` (same-origin only) instead of accepting any origin.
151
151
 
152
- > **Warning**: When both sides are Chrome extension pages under the same `chrome-extension://` origin, the default behavior is safe. However, if your iframe loads a **third-party page** or a page from a **different origin**, leaving `allowedOrigin` unset means **any window** that can reach this page can send forged `postMessage` messages and impersonate Chrome API calls. In that case, always set `allowedOrigin` to the expected origin.
152
+ > **Note**: The `'*'` fallback only affects the outgoing `postMessage` `targetOrigin` (which can be observed by anyone listening). It does **not** let arbitrary-origin messages in. Even so, prefer passing explicit `targetOrigin` and `allowedOrigin` for third-party iframes, navigable iframes, or any cross-origin page, so the message can be locked to a known destination.
153
+
154
+ > **Note**: The auto-derived `targetOrigin` is cached (`freezeResolvedOrigin`) once it resolves to a concrete origin. A fallback to `'*'` is **not** cached — the next call retries the resolution, so the common "create the iframe element first, then assign `src`" pattern doesn't end up permanently stuck on `'*'`.
153
155
 
154
156
  For production, pass origins when you know them.
155
157
 
@@ -267,7 +269,7 @@ Options:
267
269
  - `iframe`: the iframe element to bridge.
268
270
  - `key`: optional shared bridge key. Use the same key on both sides.
269
271
  - `targetOrigin`: optional origin used by `postMessage`. Falls back to a value resolved from `iframe.src`.
270
- - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages.
272
+ - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages. By default: uses the resolved `targetOrigin` when it is a concrete origin; when it falls back to `'*'`, tightens to same-origin only (`event.origin === window.location.origin`).
271
273
  - `chromeApi`: optional custom Chrome-like object. Defaults to `globalThis.chrome`.
272
274
  - `timeout`: optional per-call timeout in ms (default `30000`).
273
275
  - `functionCacheMax` / `functionCacheTtl`: tune the LRU that stores non-persistent callbacks the local side has sent.
@@ -302,3 +304,19 @@ Returns:
302
304
  - `proxy`: the same object as `chrome`.
303
305
  - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
304
306
  - `destroy()`: remove message listeners, clear bridge state, and notify the remote peer.
307
+
308
+ ### `setLogger(logger)`
309
+
310
+ Customize or silence library warnings. All internal warnings carry a `[chrome-in-iframe]` prefix and a scope tag.
311
+
312
+ ```ts
313
+ import { setLogger } from 'chrome-in-iframe';
314
+
315
+ // Silence all warnings
316
+ setLogger(null);
317
+
318
+ // Route warnings to a custom sink (e.g. Sentry)
319
+ setLogger((scope, ...args) => {
320
+ captureMessage(`[chrome-in-iframe] ${scope}`, { extra: { args } });
321
+ });
322
+ ```