chrome-in-iframe 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,169 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Bug fixes
6
+
7
+ - `handleReleaseCallbacks` now also evicts the corresponding entries from the
8
+ receiver's `remoteCallbacksByOwner` / `persistentRemoteCallbacksByOwner`
9
+ maps. Previously only the receiver's own `functionCache` /
10
+ `persistentFunctionCache` were touched, so the host kept the wrapper
11
+ function for every `chrome.*.onX.addListener(handler)` indefinitely — a
12
+ real leak in long-lived service workers with frequent listener churn.
13
+ - Reordered the `removeListener` flow on the sender side: the
14
+ `invokeRequest` is now dispatched before the `releaseCallbacks` so the
15
+ receiver can still resolve the cached wrapper, call
16
+ `chrome.*.removeListener(W)` correctly, and only afterwards drop it.
17
+ - The error response handlers (`invokeResponse`, `accessPropertyResponse`,
18
+ `invokeFunctionByIdResponse`) no longer clobber the local Error's stack
19
+ with `undefined` when the remote payload has no `stack` field.
20
+ - A function reused as a listener for several events (e.g. both
21
+ `tabs.onActivated.addListener(handler)` and
22
+ `tabs.onUpdated.addListener(handler)`) is now reference-counted on the
23
+ sender side. Removing one registration no longer evicts the shared id, so
24
+ the other listener keeps working.
25
+ - `handleInvokeRequest` rejects empty-path invocations with a clear error
26
+ instead of bottoming out at `'undefined' is not a function`.
27
+ - Mid-path read errors now report the actual parent value (`null` /
28
+ `undefined`) plus the full traversed prefix, instead of stringifying the
29
+ previous path segment.
30
+ - `isAllowedOrigin` distinguishes `undefined` (no restriction) from `''`
31
+ (empty allow-list); the empty string is no longer treated as
32
+ "allow everything".
33
+ - When `allowedOrigin` is omitted, incoming `postMessage` events are now
34
+ filtered against the same concrete origin resolved for `targetOrigin`.
35
+ This keeps the default bridge configuration from accepting messages
36
+ from an unexpected origin while still preserving the documented `'*'`
37
+ fallback when origin auto-detection cannot resolve one.
38
+ - `invoke` / `accessProperty` / `invokeFunctionById` now clear the pending
39
+ Map entry and timer when `serialize` / `sendMessage` throws synchronously.
40
+ Previously the entry lingered until the timeout fired.
41
+ - `timeout` is clamped to the default when given a non-finite or
42
+ non-positive value, so accidental `timeout: 0` no longer rejects every
43
+ call instantly.
44
+ - Listener registration/removal is now transactional. Failed
45
+ `addListener` calls roll back local persistent callback state and ask the
46
+ peer to release any wrapper it created; failed `removeListener` calls no
47
+ longer invalidate a callback that the remote side still has registered.
48
+ - Remote method and callback results now treat any thenable as async instead
49
+ of relying on `instanceof Promise`, so cross-realm Promises and custom
50
+ thenables resolve/reject correctly.
51
+ - `destroy()` no longer warns when the only failure is an already-detached
52
+ iframe whose `contentWindow` is unavailable during shutdown.
53
+ - Nested function arguments inside `addListener` payloads are now
54
+ release-tracked. Previously only top-level function arguments were
55
+ collected for cleanup, so a call like
56
+ `addListener({ filter, handler })` left `handler` pinned in the
57
+ persistent cache even after the matching `removeListener`.
58
+ - The `setTimeout` branch in `invoke` / `accessProperty` /
59
+ `invokeFunctionById` now triggers the pending entry's `onReject`
60
+ before rejecting. A timed-out `addListener` no longer leaks its
61
+ persistent registration.
62
+ - `hasListener` is no longer classified as a listener-registration path.
63
+ Its argument used to be pinned as persistent indefinitely, because
64
+ `hasListener` has no `removeListener` counterpart that would refcount
65
+ it back down.
66
+ - Sender-side function ids are now keyed by `(fn, thisArg)` rather than
67
+ just `fn`. The same function reused under different owners (e.g. one
68
+ method shared across two parent objects) no longer overwrites the
69
+ first registration's `thisArg`.
70
+ - `accessPropertyRequest` now rejects intermediate-`null` /
71
+ `undefined` traversal with the same diagnostic format as
72
+ `invokeRequest` (`Cannot read property 'X' of null (at 'a.b')`),
73
+ instead of silently returning the partial value. Leaf
74
+ `null` / `undefined` still resolves normally.
75
+ - Endpoint `destroy(notifyRemote=true)` now sends a `releaseCallbacks`
76
+ for every remote callback this endpoint is still holding before
77
+ emitting `destroyEndpoint`. This evicts the peer's persistent-flagged
78
+ methods that paths like `$get('runtime.onMessage')` had pinned on its
79
+ side — fixing a leak in long-lived hosts whose clients open and close.
80
+ - `handleReleaseCallbacks` now fully cleans `functionIds` and
81
+ `persistentRefcount` via the new `ctx.dropLocalCallback(id)`, not just
82
+ the two outer caches.
83
+ - `accessPropertyRequest` / `invokeRequest` no-delegate-target checks
84
+ now use explicit `=== undefined || === null` instead of `!target`, so
85
+ a delegate that is `0` / `false` (e.g. a numeric-rooted custom RPC
86
+ target) is no longer misreported as missing.
87
+ - `isMessageEnvelope` rejects messages whose `senderInstanceId` or
88
+ `type` is the empty string, in addition to the previous non-string
89
+ check.
90
+ - Error payload extras are now restored with own-property definitions
91
+ rather than direct assignment, so fields such as `__proto__` remain data
92
+ and cannot mutate the deserialized Error object's prototype.
93
+
94
+ ### Performance / resource
95
+
96
+ - The `message` listener now does a cheap pre-filter (string check + `'{'`
97
+ start + `"key":<jsonStringifyOfKey>` substring) before `JSON.parse`, so
98
+ unrelated `window.postMessage` traffic from other libraries is rejected
99
+ without parsing. The match string handles JSON-escaped key values, so
100
+ keys containing `"` or `\` route correctly.
101
+ - Well-known symbol reverse lookup is now O(1) via a `Map<symbol, string>`
102
+ built once at module load.
103
+ - The deep payload validators run only after `handler` lookup succeeds; the
104
+ cheap envelope check (type/key/senderInstanceId shape) runs first.
105
+ - `createWindowPoster.addEventListener` is idempotent for the same callback:
106
+ re-registering with the same listener instance unregisters the previous
107
+ DOM listener first, preventing accidental window-listener leaks.
108
+ - `handleReleaseCallbacks` caps the batch at 100 000 ids, warning and
109
+ truncating instead of silently dropping the whole batch on overflow.
110
+ - `endpoint.destroy()` is idempotent — extra calls are no-ops, so an
111
+ accidental double-destroy from cleanup hooks no longer pokes the
112
+ underlying poster twice.
113
+ - `createWindowPoster`'s listener map is keyed by `(name, callback)`
114
+ rather than just `callback`. The same callback registered against
115
+ different event names no longer overwrites each other.
116
+ - `bindMethod`'s WeakMap cache is now owned by each `ClientContext`
117
+ rather than being module-global. Multiple endpoints in the same
118
+ process no longer share bound-method state.
119
+
120
+ ### API
121
+
122
+ - New: `setLogger(logger | null)` and the `Logger` type are exported from
123
+ the package entry. Pass `null` to silence all library warnings, or pass
124
+ your own function to route them through a custom sink (e.g. Sentry).
125
+
126
+ ### Removed (internal type surface)
127
+
128
+ - The following members were removed from interfaces that are re-exported
129
+ for advanced use. They had no internal or external callers; removing them
130
+ is technically a type-level breaking change for anyone who imported and
131
+ asserted against these shapes, but the runtime behavior is unchanged:
132
+ - `ClientContext.registerPendingPromise`
133
+ - `MessageChannel.getContext`, `getPoster`, `getKey`, `getInstanceId`
134
+
135
+ ### Logging
136
+
137
+ - Extracted a unified `warn()` utility (`src/log.ts`) with a `[chrome-in-iframe]` prefix and scope tag for all internal warnings.
138
+ - 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`).
139
+ - Migrated existing ad-hoc `console.warn` calls to use the centralized `warn()` utility.
140
+
141
+ ### Package metadata
142
+
143
+ - `exports` map now exposes top-level `types` / `import` / `default`
144
+ conditions and `"./package.json"` for tooling that wants to read the
145
+ manifest. Fixes resolution under stricter bundlers (webpack 5 strict
146
+ exports) without changing the canonical entry.
147
+ - Removed the `clean` and `test:watch` scripts. `build` now runs `rollup -c`
148
+ directly without a preceding `clean` step.
149
+
150
+ ### Tests
151
+
152
+ - New `test/chrome-api.test.ts` with 53 tests covering all 12 Chrome MV3 API
153
+ patterns through the bridge: Promise async methods (various return types),
154
+ synchronous methods proxied as async, Port objects, event listeners
155
+ (addListener/removeListener/hasListener, multi-arg callbacks, sendResponse),
156
+ nested namespaces, optional leading parameters, static properties via `$get`,
157
+ complex data round-trips, CRUD registration, multi-client event isolation,
158
+ error propagation, and timeout behavior.
159
+ - 8 new regression tests in `test/bridge.test.ts` covering each of the
160
+ newly fixed defects: nested-callback release on `removeListener`,
161
+ persistent rollback on `addListener` timeout, non-persistent
162
+ `hasListener` argument, mid-path null/undefined diagnostics through
163
+ `$get`, distinct `(fn, thisArg)` entries for the same function, host
164
+ persistent-cache cleanup on client `destroy()`, idempotent
165
+ `endpoint.destroy()`, and empty-`senderInstanceId` envelope rejection.
166
+
3
167
  ## 2.0.0
4
168
 
5
169
  Breaking changes — both peers of the bridge must run the same major version.
@@ -1,5 +1,136 @@
1
1
  # 更新日志
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Bug 修复
6
+
7
+ - `handleReleaseCallbacks` 现在会同时清理接收方 `remoteCallbacksByOwner` /
8
+ `persistentRemoteCallbacksByOwner` 中对应的条目。此前只清理接收方自身
9
+ 的 `functionCache` / `persistentFunctionCache`,导致每次
10
+ `chrome.*.onX.addListener(handler)` 在宿主端留下的包裹函数永远不会被
11
+ 释放 —— 在长期运行的 service worker 中频繁增删监听器会造成真实泄漏。
12
+ - 调整 `removeListener` 的客户端发送顺序:先发送 `invokeRequest`,再发送
13
+ `releaseCallbacks`。这样接收方可以先用缓存中的包裹函数调用
14
+ `chrome.*.removeListener(W)`,然后再清理缓存。
15
+ - 错误响应处理(`invokeResponse` / `accessPropertyResponse` /
16
+ `invokeFunctionByIdResponse`)不再在远端 payload 没有 `stack` 字段时
17
+ 用 `undefined` 覆盖本地 `Error` 的 stack。
18
+ - 同一个函数被注册为多个事件的监听器(如同时绑定
19
+ `tabs.onActivated.addListener(handler)` 和
20
+ `tabs.onUpdated.addListener(handler)`)时,客户端现在按 callback id
21
+ 做引用计数。移除其中一处不再让另一处失效。
22
+ - `handleInvokeRequest` 在 path 为空时直接返回明确的错误,而不是落到
23
+ `'undefined' is not a function`。
24
+ - 中间路径解到 null / undefined 时的错误信息现在准确报告父值类型,并附
25
+ 上已访问的路径前缀。
26
+ - `isAllowedOrigin` 区分 `undefined`(不限制)与 `''`(空白名单)。空字符串
27
+ 不再被当作"放行所有 origin"。
28
+ - 未传 `allowedOrigin` 时,传入的 `postMessage` 现在会默认按
29
+ `targetOrigin` 推导出的同一个具体 origin 过滤。这样默认桥接配置不会
30
+ 接收来自意外 origin 的消息,同时仍保留自动推导失败时回退到 `'*'` 的
31
+ 既有行为。
32
+ - `invoke` / `accessProperty` / `invokeFunctionById` 在 `serialize` /
33
+ `sendMessage` 同步抛错时会清掉 pending 条目和 timer,不再等到 timeout
34
+ 才回收。
35
+ - 非有限或非正的 `timeout` 值会回落到默认值,避免 `timeout: 0` 导致每
36
+ 次调用瞬间被拒。
37
+ - 监听器注册/移除现在是事务式的。失败的 `addListener` 会回滚本地持久
38
+ callback 状态,并通知对端释放已创建的包裹函数;失败的
39
+ `removeListener` 不再让仍注册在远端的 callback 失效。
40
+ - 远端方法和 callback 返回值现在按 thenable 判断异步,不再依赖
41
+ `instanceof Promise`,因此跨 realm Promise 和自定义 thenable 可以正确
42
+ resolve/reject。
43
+ - `destroy()` 在关闭阶段如果只是因为 iframe 已分离、`contentWindow`
44
+ 不可用而通知失败,不再输出 warning。
45
+ - `addListener` 参数中嵌套对象内的回调函数现在也参与释放计数。此前
46
+ 只有顶层函数参数会被收集到回滚清单中,所以
47
+ `addListener({ filter, handler })` 这种调用即使后续 `removeListener`,
48
+ `handler` 也会一直留在持久缓存里。
49
+ - `invoke` / `accessProperty` / `invokeFunctionById` 的 `setTimeout`
50
+ 分支现在会在 reject 之前触发挂起项的 `onReject`。`addListener`
51
+ 调用超时不再造成持久注册泄漏。
52
+ - `hasListener` 不再被识别为监听器注册路径。其参数此前会被永久标记
53
+ 为 persistent —— 因为 `hasListener` 没有对应的 `removeListener`
54
+ 可以把引用计数减回去。
55
+ - 客户端的函数 id 现在以 `(fn, thisArg)` 为联合键,而不是仅以 `fn`
56
+ 为键。同一函数被复用到不同 owner 上(例如同一方法挂在两个父对象
57
+ 上)不再覆盖前一次注册的 `thisArg`。
58
+ - `accessPropertyRequest` 中间节点解到 null / undefined 时,现在
59
+ 和 `invokeRequest` 一样抛出 `Cannot read property 'X' of null
60
+ (at 'a.b')` 的诊断错误,不再静默返回中间值。叶子节点为
61
+ null / undefined 仍正常返回。
62
+ - 端点 `destroy(notifyRemote=true)` 现在会在发送 `destroyEndpoint`
63
+ 之前,先发送一条 `releaseCallbacks`,列出本端当前持有的所有远端
64
+ callback id。这样对端就可以释放被 `$get('runtime.onMessage')`
65
+ 这类路径标记为 persistent 的方法 —— 修复了长期运行宿主里客户端
66
+ 反复 open/close 时的累积泄漏。
67
+ - `handleReleaseCallbacks` 现在通过新增的 `ctx.dropLocalCallback(id)`
68
+ 彻底清理 `functionIds` 和 `persistentRefcount`,而不是只清理外层
69
+ 两个缓存。
70
+ - `accessPropertyRequest` / `invokeRequest` 的"无 delegate target"
71
+ 判断改为显式 `=== undefined || === null`,不再用 `!target`。这样
72
+ `0` / `false`(例如以数字为根的自定义 RPC target)不会被误报为
73
+ "未配置 delegate target"。
74
+ - `isMessageEnvelope` 在原有的非字符串校验之外,还会拒绝
75
+ `senderInstanceId` / `type` 为空字符串的消息。
76
+ - Error payload 的额外字段现在通过 own-property 定义恢复,而不是直接赋值。
77
+ 因此 `__proto__` 等字段会被保留为普通数据,不会改变反序列化后 Error
78
+ 对象的原型。
79
+
80
+ ### 性能 / 资源
81
+
82
+ - `message` 监听器在 `JSON.parse` 之前先做廉价过滤(字符串检查 + `'{'`
83
+ 起始 + `"key":<JSON.stringify(key)>` 子串)。其它库通过
84
+ `window.postMessage` 发出的无关消息不再被解析。匹配串经过 JSON 转义,
85
+ 含 `"` 或 `\` 的 key 也能正确路由。
86
+ - 知名 Symbol 反向查找改为模块加载时一次性建立的 `Map<symbol, string>`,
87
+ O(1) 替代之前的 O(n)。
88
+ - envelope(type / key / senderInstanceId 形状)的廉价校验先做;深层
89
+ payload 校验延后到 handler 命中之后再执行。
90
+ - `createWindowPoster.addEventListener` 对同一 callback 幂等:再次注册
91
+ 会先卸掉旧的 DOM listener,避免 window-listener 泄漏。
92
+ - `handleReleaseCallbacks` 将单批上限改为 100 000 个 id,超过时截断
93
+ 并输出警告,而不是直接丢弃整批。
94
+ - `endpoint.destroy()` 改为幂等——重复调用直接 no-op,避免清理钩子
95
+ 里不小心的 double-destroy 让底层 poster 被处理两次。
96
+ - `createWindowPoster` 的 listener map 改为以 `(name, callback)` 为
97
+ 联合键,不再仅以 `callback` 为键。同一 callback 注册到不同事件名
98
+ 时不会再相互覆盖。
99
+ - `bindMethod` 的 WeakMap 缓存改由每个 `ClientContext` 各自持有,
100
+ 不再是模块级全局。同一进程中多个端点不再共享绑定方法状态。
101
+
102
+ ### API
103
+
104
+ - 新增:从包入口导出 `setLogger(logger | null)` 和 `Logger` 类型。传
105
+ `null` 可彻底静默库内警告,也可传入自定义函数把日志路由到 Sentry 等
106
+ 外部 sink。
107
+
108
+ ### 移除(内部类型层面)
109
+
110
+ - 以下接口成员被移除。它们在源码和测试中都没有调用方,从未被实际使
111
+ 用;运行时行为完全不变,仅在严格断言这些接口形状的下游代码看来属
112
+ 于类型层面的破坏性变更:
113
+ - `ClientContext.registerPendingPromise`
114
+ - `MessageChannel.getContext` / `getPoster` / `getKey` / `getInstanceId`
115
+
116
+ ### 日志
117
+
118
+ - 提取统一的 `warn()` 工具函数(`src/log.ts`),所有内部警告均带有 `[chrome-in-iframe]` 前缀和 scope 标签。
119
+ - 将所有静默 `catch` 块(仅注释 / 空 `return`)替换为 `warn()` 调用,此前被吞掉的错误现在会在控制台输出。涉及区域:源推导(`deriveOriginFromIframe`、`deriveParentOrigin`、`parseOrigin`)、消息分发(`createMessageChannel`)、反序列化(`deserializeError`、`deserializeWrappedObject`、`resolveObjectKey`)以及客户端生命周期(`notifyReleaseCallbacks`、`destroy`)。
120
+ - 将现有的裸 `console.warn` 调用迁移至集中的 `warn()` 工具。
121
+
122
+ ### 包元数据
123
+
124
+ - `exports` map 新增顶层 `types` / `import` / `default` 条件以及
125
+ `"./package.json"` 子路径,修复 webpack 5 严格 exports 等工具链下的
126
+ 解析问题。规范入口未变。
127
+ - 移除 `clean` 和 `test:watch` 脚本,`build` 直接运行 `rollup -c`,不再前置清理步骤。
128
+
129
+ ### 测试
130
+
131
+ - 新增 `test/chrome-api.test.ts`,包含 53 个测试用例,覆盖全部 12 种 Chrome MV3 API 调用模式的桥接测试:Promise 异步方法(各种返回类型)、同步方法代理、Port 对象、事件监听器(addListener/removeListener/hasListener、多参数回调、sendResponse)、嵌套命名空间、可选前导参数、$get 静态属性、复杂数据往返、CRUD 注册模式、多客户端事件隔离、错误传播、超时行为。
132
+ - 在 `test/bridge.test.ts` 末尾新增 8 个针对上述新修复点的回归测试:`removeListener` 时嵌套回调被释放、`addListener` 超时回滚持久注册、`hasListener` 参数不被标记为 persistent、通过 `$get` 时中间路径 null/undefined 的诊断错误、同一函数不同 owner 产生不同 entry、客户端 `destroy()` 触发宿主端持久缓存清零、`endpoint.destroy()` 幂等、空 `senderInstanceId` 信封被拒收。
133
+
3
134
  ## 2.0.0
4
135
 
5
136
  破坏性变更 — 桥接的两端必须运行相同的主版本号。
@@ -8,7 +139,7 @@
8
139
 
9
140
  - 将魔法字符串前缀(`$undefined$`、`$function$id` 等)替换为对象标签(`{ $cii$: 'undef' }`、`{ $cii$: 'fn', id }` 等)。与保留标记冲突的普通字符串现在可以正确地往返传输。
10
141
  - 新增对 `Date`、`RegExp`、`Map`、`Set`、`BigInt`、`NaN`、`Infinity`、`-Infinity` 和 `-0` 的序列化支持。
11
- - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有品牌 Symbol 的用户对象不再导致序列化抛出异常。
142
+ - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有 brand Symbol 的用户对象不再导致序列化抛出异常。
12
143
  - `Error` 序列化保留 `name`(因此 `TypeError` 传输后仍为 `TypeError`)、`cause`(递归)、`stack` 以及额外的自有可枚举字符串键属性(如自定义 `code` 字段)。
13
144
  - 标签载荷会进行校验;格式错误的 `date` / `regexp` / `bigint` / `fn` 载荷会抛出带有描述信息的 `TypeError`,而不是产生 `Invalid Date` 或静默返回 undefined。当条目包含无法解析的键时,`deserializeWrappedObject` 会向控制台输出警告,而非静默丢弃。
14
145
 
package/README.md CHANGED
@@ -147,9 +147,9 @@ 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`.
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
+ > **Warning**: If origin auto-detection cannot resolve a concrete origin and falls back to `'*'`, incoming messages from the expected window source are allowed from any origin. For third-party iframes, navigable iframes, or any page from a different origin, pass explicit `targetOrigin` and `allowedOrigin`.
153
153
 
154
154
  For production, pass origins when you know them.
155
155
 
@@ -267,7 +267,7 @@ Options:
267
267
  - `iframe`: the iframe element to bridge.
268
268
  - `key`: optional shared bridge key. Use the same key on both sides.
269
269
  - `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.
270
+ - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages. Defaults to the resolved `targetOrigin` when it is a concrete origin.
271
271
  - `chromeApi`: optional custom Chrome-like object. Defaults to `globalThis.chrome`.
272
272
  - `timeout`: optional per-call timeout in ms (default `30000`).
273
273
  - `functionCacheMax` / `functionCacheTtl`: tune the LRU that stores non-persistent callbacks the local side has sent.
@@ -302,3 +302,19 @@ Returns:
302
302
  - `proxy`: the same object as `chrome`.
303
303
  - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
304
304
  - `destroy()`: remove message listeners, clear bridge state, and notify the remote peer.
305
+
306
+ ### `setLogger(logger)`
307
+
308
+ Customize or silence library warnings. All internal warnings carry a `[chrome-in-iframe]` prefix and a scope tag.
309
+
310
+ ```ts
311
+ import { setLogger } from 'chrome-in-iframe';
312
+
313
+ // Silence all warnings
314
+ setLogger(null);
315
+
316
+ // Route warnings to a custom sink (e.g. Sentry)
317
+ setLogger((scope, ...args) => {
318
+ captureMessage(`[chrome-in-iframe] ${scope}`, { extra: { args } });
319
+ });
320
+ ```
package/README.zh-CN.md CHANGED
@@ -147,9 +147,9 @@ window.addEventListener('beforeunload', () => {
147
147
 
148
148
  ## 安全选项
149
149
 
150
- `targetOrigin` 和 `allowedOrigin` 是可选的。当不传 `targetOrigin` 时,库会自动从 `iframe.src` / `contentWindow.location.origin`(扩展侧)或 `document.referrer` / `location.ancestorOrigins`(iframe 侧)推导出 origin;只有在以上来源都不可用时才回退到 `'*'`。传入的消息还会通过预期的 window source 进行校验。
150
+ `targetOrigin` 和 `allowedOrigin` 是可选的。当不传 `targetOrigin` 时,库会自动从 `iframe.src` / `contentWindow.location.origin`(扩展侧)或 `document.referrer` / `location.ancestorOrigins`(iframe 侧)推导出 origin;只有在以上来源都不可用时才回退到 `'*'`。传入的消息默认同时校验预期的 window source,以及为 `targetOrigin` 推导出的同一个 origin。
151
151
 
152
- > **警告**:当两侧都是同一 `chrome-extension://` origin 下的 Chrome 扩展页面时,默认行为是安全的。但如果 iframe 加载的是**第三方页面**或来自**不同 origin** 的页面,不设置 `allowedOrigin` 意味着**任何能向页面发送消息的窗口**都可以伪造 `postMessage` 来冒充 Chrome API 调用。在这种情况下,务必将 `allowedOrigin` 设置为预期的 origin。
152
+ > **警告**:如果自动推导无法得到具体 origin 并回退到 `'*'`,来自预期 window source 的传入消息会允许任意 origin。对于第三方 iframe、可能被导航的 iframe,或任何不同 origin 的页面,请显式传入 `targetOrigin` `allowedOrigin`。
153
153
 
154
154
  在生产环境中,如果知道具体来源,建议显式传入 origin。
155
155
 
@@ -180,10 +180,10 @@ const { chrome } = connectChromeInIframe({
180
180
 
181
181
  任何 RPC 桥接都有的固有限制:
182
182
 
183
- - **对象身份不被保留。** 跨桥的 `Date`、`Map`、`Set`、普通对象会在对面被重新构造。原对象和 round-trip 后的对象之间 `===` 为 `false`。
184
- - **以对象为 key 的 Map / Set 会丢失查找能力。** 把 `Date`(或任何对象)作为 `Map` 的 key,跨桥之后,对面 `map.get(originalDate)` 找不到——反序列化得到的 `Map` 里持有的是另一个 `Date` 实例。需要跨桥传输的 `Map`,请用字符串或数字作 key。
185
- - **非全局 symbol 不能跨桥。** 用 `Symbol.for(...)` 或 well-known symbol;临时 `Symbol('local')` 在序列化时抛错。
186
- - **类原型会被擦除。** `class Foo { ... }` 的实例到对面只是一个普通对象,只保留可枚举字段;`instanceof Foo` 为 `false`。
183
+ - **对象身份不被保留。** 跨桥的 `Date`、`Map`、`Set`、普通对象会在对端被重新构造。原对象和往返后的对象之间 `===` 为 `false`。
184
+ - **以对象为 key 的 Map / Set 会丢失查找能力。** 把 `Date`(或任何对象)作为 `Map` 的 key,跨桥之后,对端 `map.get(originalDate)` 找不到 —— 反序列化得到的 `Map` 里持有的是另一个 `Date` 实例。需要跨桥传输的 `Map`,请用字符串或数字作 key。
185
+ - **非全局 symbol 不能跨桥。** 用 `Symbol.for(...)` 或 well-known symbol;临时 `Symbol('local')` 在序列化时抛错。
186
+ - **类原型会被擦除。** `class Foo { ... }` 的实例到对端只是一个普通对象,只保留可枚举字段;`instanceof Foo` 为 `false`。
187
187
 
188
188
  ## 读取属性
189
189
 
@@ -267,11 +267,11 @@ const bridge = exposeChromeInIframe({
267
267
  - `iframe`:要桥接的 iframe 元素。
268
268
  - `key`:可选的共享桥接 key。两侧使用相同的 key。
269
269
  - `targetOrigin`:可选,`postMessage` 使用的 origin。不传时由 `iframe.src` 推导。
270
- - `allowedOrigin`:可选,用于过滤传入消息的 origin、origin 列表或判断函数。
270
+ - `allowedOrigin`:可选,用于过滤传入消息;可以是 origin、origin 列表或判断函数。默认使用解析后的 `targetOrigin`(当它是具体 origin 时)。
271
271
  - `chromeApi`:可选,自定义的 Chrome API 对象。默认为 `globalThis.chrome`。
272
272
  - `timeout`:可选,单次调用的超时时长(毫秒,默认 `30000`)。
273
- - `functionCacheMax` / `functionCacheTtl`:调整本地存储的非持久 callback LRU
274
- - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`:调整对端交给我们的 callback LRU
273
+ - `functionCacheMax` / `functionCacheTtl`:调整本端发出的非持久 callback LRU 缓存。
274
+ - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`:调整对端发给本端的 callback LRU 缓存。
275
275
 
276
276
  返回值:
277
277
 
@@ -302,3 +302,19 @@ const { chrome, get, destroy } = connectChromeInIframe({
302
302
  - `proxy`:与 `chrome` 相同的对象。
303
303
  - `get(path)`:从桥接的 Chrome API 中读取属性。详见 [路径形式](#路径形式)。
304
304
  - `destroy()`:移除消息监听器,清理桥接状态,并通知对端。
305
+
306
+ ### `setLogger(logger)`
307
+
308
+ 自定义或静默库内警告。所有内部警告均带有 `[chrome-in-iframe]` 前缀和 scope 标签。
309
+
310
+ ```ts
311
+ import { setLogger } from 'chrome-in-iframe';
312
+
313
+ // 静默所有警告
314
+ setLogger(null);
315
+
316
+ // 将警告路由到自定义 sink(如 Sentry)
317
+ setLogger((scope, ...args) => {
318
+ captureMessage(`[chrome-in-iframe] ${scope}`, { extra: { args } });
319
+ });
320
+ ```
@@ -64,6 +64,8 @@ export interface PromiseCallbacks {
64
64
  resolve: (value: MessageValue) => void;
65
65
  reject: (reason: MessageValue) => void;
66
66
  timer: ReturnType<typeof setTimeout>;
67
+ onResolve?: () => void;
68
+ onReject?: () => void;
67
69
  }
68
70
  export interface ClientContext {
69
71
  getDelegateTarget(): MessageValue;
@@ -77,8 +79,15 @@ export interface ClientContext {
77
79
  invoke(path: PathKey[], args: MessageValue[]): Promise<MessageValue>;
78
80
  accessProperty(path: PathKey[]): Promise<MessageValue>;
79
81
  invokeFunctionById(id: string, args: MessageValue[], options?: CallbackCacheOptions): Promise<MessageValue>;
80
- registerPendingPromise(id: RequestId, callbacks: PromiseCallbacks): void;
81
82
  handleRemoteDestroy(instanceId: string): void;
83
+ releaseRemoteCallbacks(remoteInstanceId: string, ids: readonly string[]): void;
84
+ dropLocalCallback(id: string): void;
85
+ bindMethod(fn: Callable, owner: MessageValue): Callable;
86
+ /** @internal Exposed for tests; do not use in application code. */
87
+ getRemoteCallbackCounts(remoteInstanceId: string): {
88
+ lru: number;
89
+ persistent: number;
90
+ };
82
91
  destroy(notifyRemote?: boolean): void;
83
92
  }
84
93
  export interface CallbackCacheOptions {
@@ -90,10 +99,6 @@ export interface CallbackEntry {
90
99
  }
91
100
  export interface MessageChannel {
92
101
  getSender(): MessageSender;
93
- getContext(): ClientContext;
94
- getPoster(): MessagePoster;
95
- getKey(): string;
96
- getInstanceId(): string;
97
102
  destroy(): void;
98
103
  }
99
104
  export interface MessageSender {
@@ -3,3 +3,4 @@ export declare const WELL_KNOWN_SYMBOLS: Record<string, symbol>;
3
3
  export declare function getWellKnownSymbolName(value: symbol): string | undefined;
4
4
  export declare function getWellKnownSymbol(name: string): symbol | undefined;
5
5
  export declare function serializeThrownError(err: unknown): SerializedError;
6
+ export declare function isPromiseLike(value: unknown): value is PromiseLike<unknown>;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { connectChromeInIframe, exposeChromeInIframe, setupInIframe, setupInMainWindow } from './api';
2
+ export { setLogger } from './log';
3
+ export type { Logger } from './log';
2
4
  export type { AllowedOrigin, PropertyAccess, PropertyPathPart, ConnectionHandle, ChromeIframeHandle, ExposeChromeInIframeOptions, SetupInIframeOptions, SetupInMainWindowOptions, ConnectionOptions, } from './api';
3
5
  export type { Messages, MessageType, MessageBody, MessagePoster, ClientContext, MessageChannel, MessageSender, ProxyNode, ListenerCallback, SerializedError, } from './channel/types';