chrome-in-iframe 1.0.0 → 2.0.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 ADDED
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0
4
+
5
+ Breaking changes — both peers of the bridge must run the same major version.
6
+
7
+ ### Serialization protocol rewrite
8
+
9
+ - Replaced magic-string prefixes (`$undefined$`, `$function$id`, …) with
10
+ object tags (`{ $cii$: 'undef' }`, `{ $cii$: 'fn', id }`, …). Plain strings
11
+ that previously collided with reserved markers now round-trip correctly.
12
+ - Added serialization support for `Date`, `RegExp`, `Map`, `Set`, `BigInt`,
13
+ `NaN`, `Infinity`, `-Infinity`, and `-0`.
14
+ - Plain objects whose own keys include the reserved `$cii$` marker or contain
15
+ symbol-typed keys are wrapped as entry lists, so they round-trip without
16
+ ambiguity. `Reflect.ownKeys` is used so own symbol keys are preserved.
17
+ Non-global `Symbol(...)` keys are silently skipped (`Symbol.for(...)` and
18
+ well-known symbols still round-trip), so user objects with private brand
19
+ symbols no longer cause serialization to throw.
20
+ - `Error` serialization preserves `name` (so `TypeError` arrives as a
21
+ `TypeError`), `cause` (recursive), `stack`, and additional own enumerable
22
+ string-keyed properties (e.g. custom `code` fields).
23
+ - Tagged payloads are validated; malformed `date` / `regexp` / `bigint` /
24
+ `fn` payloads throw informative `TypeError`s instead of producing
25
+ `Invalid Date` or silently undefined values. `deserializeWrappedObject`
26
+ warns to the console when an entry has an unresolvable key instead of
27
+ silently dropping it.
28
+
29
+ ### Memory and lifecycle
30
+
31
+ - `removeListener(callback)` clears the local persistent function cache and
32
+ asks the remote peer to drop its `remoteCallbacks` entry. Long-running
33
+ add/remove cycles no longer leak callbacks.
34
+ - `destroy()` notifies the remote peer so it can release callbacks it holds
35
+ on behalf of this endpoint.
36
+ - Each endpoint gets a unique `instanceId`; responses are addressed to the
37
+ originating endpoint so multiple clients sharing a key no longer process
38
+ duplicate requests or responses.
39
+ - Remote callback caches are partitioned by remote `instanceId`. When one
40
+ client of a shared-key bridge calls `destroy()`, only the callbacks owned
41
+ by that client are released — peers (other clients) keep working.
42
+ `handleDestroyEndpoint` uses the message envelope's `senderInstanceId`
43
+ rather than wire-level `data.instanceId`, so a peer cannot forge a
44
+ destroy message that targets another endpoint's cache.
45
+ - `handleReleaseCallbacks` caps batches at 10 000 ids to bound worst-case
46
+ work from malformed or hostile messages.
47
+ - Nested callbacks no longer inherit the outer `persistent` flag through
48
+ deserialization; the wire `persistent` field is the sole source of truth.
49
+ - Non-`on[A-Z]` methods returned via `$get` are no longer flagged
50
+ `persistent`, so short-lived methods are LRU-evicted instead of pinned
51
+ forever.
52
+ - `functionIds` is cleaned up when the underlying `functionCache` LRU
53
+ evicts an entry, so long-lived endpoints that register many short-lived
54
+ callbacks no longer accumulate stale `(fn → id)` mappings.
55
+ - `releaseCallbacks` is dispatched synchronously rather than via
56
+ `queueMicrotask`, eliminating a race where a `removeListener` invoke
57
+ request could outrun the corresponding release notification.
58
+ - Response handlers (`invokeResponse`, `accessPropertyResponse`,
59
+ `invokeFunctionByIdResponse`) reject the pending promise if
60
+ deserialization throws (e.g. corrupted wire payload). Previously the
61
+ pending entry was already cleared by the lookup, leaving the promise
62
+ permanently unresolved.
63
+
64
+ ### Method binding
65
+
66
+ - Methods returned through `$get(path)` are bound to their owner before
67
+ serialization, so calling them via the proxy no longer loses `this`.
68
+ - Methods reached through normal `chrome.x.y.method()` invocations are
69
+ applied with `Reflect.apply(fn, owner, args)` on the host side as well.
70
+ - Methods retrieved via `$get` are bound once per `(method, owner)` pair,
71
+ so repeat access reuses the same callback id instead of bloating the
72
+ cache. Non-object owners are not cached (they cannot occur via real
73
+ property paths but would otherwise produce misleading shared bindings).
74
+
75
+ ### Security defaults
76
+
77
+ - `targetOrigin` defaults to the iframe's resolved origin (parsed from
78
+ `iframe.src` or `contentWindow.location.origin`). The iframe side derives
79
+ the parent origin from `document.referrer` / `location.ancestorOrigins`.
80
+ When automatic derivation fails (cross-origin without referrer info), the
81
+ resolver warns and falls back to `'*'`; previously it incorrectly fell
82
+ back to the current window's origin, which would silently drop messages
83
+ bound for a different origin iframe.
84
+
85
+ ### Proxy ergonomics
86
+
87
+ - The client proxy returns `undefined` for common host-introspection paths
88
+ (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`,
89
+ `Symbol.iterator`, `Symbol.isConcatSpreadable`, `Symbol.toStringTag`, …)
90
+ and for `Function.prototype` reflection fields (`length`, `name`,
91
+ `prototype`, `arguments`, `caller`, `bind`, `call`, `apply`). Reflection
92
+ libraries and `Function.prototype.bind.call(proxy, …)` no longer create
93
+ spurious child proxy nodes.
94
+ - `Symbol.toPrimitive` returns a function that produces
95
+ `'[chrome-in-iframe proxy]'`, so `String(proxy)` and template strings no
96
+ longer throw.
97
+ - All proxy nodes share a single no-op target function to avoid per-access
98
+ allocation.
99
+
100
+ ### Configuration
101
+
102
+ - New options on `ConnectionOptions`: `functionCacheMax`, `functionCacheTtl`,
103
+ `remoteCallbackCacheMax`, `remoteCallbackCacheTtl`. Tune them when you
104
+ need callbacks that live longer than the 5-minute LRU default.
105
+
106
+ ### `$get` path semantics
107
+
108
+ - Only the single-string form splits on `.` (e.g. `$get('runtime.id')`).
109
+ Multi-argument calls now treat each argument as a single path segment, so
110
+ keys that contain `.` (e.g. `'flag.v2'`) are addressable via
111
+ `$get('storage', 'local', 'flag.v2')` or the array form
112
+ `$get(['storage', 'local', 'flag.v2'])`.
113
+ - Previously, every string argument was split on `.`, which made dotted
114
+ keys unreachable through `get` / `$get`.
115
+
116
+ ### Module format
117
+
118
+ - The package is **ESM-only**. There is no CommonJS entry point.
119
+ `require('chrome-in-iframe')` will not work — use it from an ESM project
120
+ or configure your bundler to treat it as an ESM external.
121
+
122
+ ### Package metadata
123
+
124
+ - Added `engines`, `sideEffects: false`.
125
+ - `prepublishOnly` runs `typecheck`, `test`, then `build`.
126
+ - `lint` covers both `src` and `test`.
127
+ - Bundled `LICENSE` and `CHANGELOG.md` into the published tarball.
128
+ - `Endpoint.getContext` is marked `@internal` — it is only retained for
129
+ tests and should not be used by application code.
130
+
131
+ ## 1.0.1
132
+
133
+ - Upgrade `nanoid` to `^5.1.11`.
134
+ - Update the example extension ID in the README from
135
+ `chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb` to
136
+ `chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc`.
137
+
138
+ ## 1.0.0
139
+
140
+ Initial public release.
@@ -0,0 +1,72 @@
1
+ # 更新日志
2
+
3
+ ## 2.0.0
4
+
5
+ 破坏性变更 — 桥接的两端必须运行相同的主版本号。
6
+
7
+ ### 序列化协议重写
8
+
9
+ - 将魔法字符串前缀(`$undefined$`、`$function$id` 等)替换为对象标签(`{ $cii$: 'undef' }`、`{ $cii$: 'fn', id }` 等)。与保留标记冲突的普通字符串现在可以正确地往返传输。
10
+ - 新增对 `Date`、`RegExp`、`Map`、`Set`、`BigInt`、`NaN`、`Infinity`、`-Infinity` 和 `-0` 的序列化支持。
11
+ - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有品牌 Symbol 的用户对象不再导致序列化抛出异常。
12
+ - `Error` 序列化保留 `name`(因此 `TypeError` 传输后仍为 `TypeError`)、`cause`(递归)、`stack` 以及额外的自有可枚举字符串键属性(如自定义 `code` 字段)。
13
+ - 标签载荷会进行校验;格式错误的 `date` / `regexp` / `bigint` / `fn` 载荷会抛出带有描述信息的 `TypeError`,而不是产生 `Invalid Date` 或静默返回 undefined。当条目包含无法解析的键时,`deserializeWrappedObject` 会向控制台输出警告,而非静默丢弃。
14
+
15
+ ### 内存与生命周期
16
+
17
+ - `removeListener(callback)` 会清除本地持久化函数缓存,并通知远端对等节点丢弃其 `remoteCallbacks` 条目。长时间运行的添加/移除循环不再泄漏回调函数。
18
+ - `destroy()` 会通知远端对等节点,使其可以释放为该端点持有的回调函数。
19
+ - 每个端点获得唯一的 `instanceId`;响应会被发送到发起请求的端点,因此共享同一个 key 的多个客户端不再处理重复的请求或响应。
20
+ - 远端回调缓存按远端 `instanceId` 进行分区。当共享 key 的桥接中某个客户端调用 `destroy()` 时,只有该客户端拥有的回调会被释放 — 其他客户端的对等节点继续正常工作。`handleDestroyEndpoint` 使用消息信封中的 `senderInstanceId` 而非线路层的 `data.instanceId`,因此对等节点无法伪造针对其他端点缓存的销毁消息。
21
+ - `handleReleaseCallbacks` 将批量操作限制为最多 10,000 个 ID,以限制来自格式错误或恶意消息的最大工作量。
22
+ - 嵌套回调不再通过反序列化继承外层的 `persistent` 标志;线路上的 `persistent` 字段是唯一的来源。
23
+ - 通过 `$get` 返回的非 `on[A-Z]` 方法不再被标记为 `persistent`,因此短期方法会被 LRU 淘汰而非永久保留。
24
+ - 当底层 `functionCache` LRU 淘汰条目时,`functionIds` 会被同步清理,因此注册了许多短期回调的长期端点不再积累过期的 `(fn → id)` 映射。
25
+ - `releaseCallbacks` 现在同步派发而非通过 `queueMicrotask`,消除了 `removeListener` 调用请求可能抢先于对应释放通知的竞态条件。
26
+ - 响应处理器(`invokeResponse`、`accessPropertyResponse`、`invokeFunctionByIdResponse`)在反序列化抛出异常时(如损坏的线路载荷)会拒绝挂起的 Promise。此前,挂起条目在查找时已被清除,导致 Promise 永远无法 resolve。
27
+
28
+ ### 方法绑定
29
+
30
+ - 通过 `$get(path)` 返回的方法在序列化前会绑定到其所有者对象,因此通过代理调用它们不再丢失 `this`。
31
+ - 通过正常 `chrome.x.y.method()` 调用链到达的方法在宿主端也会使用 `Reflect.apply(fn, owner, args)` 进行调用。
32
+ - 通过 `$get` 检索的方法按 `(method, owner)` 对绑定一次,因此重复访问会复用相同的回调 ID 而不会使缓存膨胀。非对象类型的所有者不会被缓存(它们不会通过真实的属性路径出现,否则会产生误导性的共享绑定)。
33
+
34
+ ### 安全默认值
35
+
36
+ - `targetOrigin` 默认为 iframe 解析后的源(从 `iframe.src` 或 `contentWindow.location.origin` 解析)。iframe 侧从 `document.referrer` / `location.ancestorOrigins` 推导父级源。当自动推导失败时(跨域且无 referrer 信息),解析器会发出警告并回退到 `'*'`;此前它错误地回退到当前窗口的源,这会静默丢弃发往不同源 iframe 的消息。
37
+
38
+ ### 代理易用性
39
+
40
+ - 客户端代理对常见的宿主内省路径(`then`、`catch`、`finally`、`toJSON`、`toString`、`valueOf`、`Symbol.iterator`、`Symbol.isConcatSpreadable`、`Symbol.toStringTag` 等)以及 `Function.prototype` 反射字段(`length`、`name`、`prototype`、`arguments`、`caller`、`bind`、`call`、`apply`)返回 `undefined`。反射库和 `Function.prototype.bind.call(proxy, …)` 不再创建多余的子代理节点。
41
+ - `Symbol.toPrimitive` 返回一个生成 `'[chrome-in-iframe proxy]'` 的函数,因此 `String(proxy)` 和模板字符串不再抛出异常。
42
+ - 所有代理节点共享一个单一的无操作目标函数,避免每次访问时的内存分配。
43
+
44
+ ### 配置
45
+
46
+ - `ConnectionOptions` 新增选项:`functionCacheMax`、`functionCacheTtl`、`remoteCallbackCacheMax`、`remoteCallbackCacheTtl`。当需要回调存活超过 5 分钟的 LRU 默认值时,可以调整这些参数。
47
+
48
+ ### `$get` 路径语义
49
+
50
+ - 只有单字符串形式才会按 `.` 分割(如 `$get('runtime.id')`)。多参数调用现在将每个参数视为单个路径段,因此包含 `.` 的键(如 `'flag.v2'`)可以通过 `$get('storage', 'local', 'flag.v2')` 或数组形式 `$get(['storage', 'local', 'flag.v2'])` 访问。
51
+ - 此前,每个字符串参数都会按 `.` 分割,导致带点的键无法通过 `get` / `$get` 访问。
52
+
53
+ ### 模块格式
54
+
55
+ - 本包**仅支持 ESM**。没有 CommonJS 入口。`require('chrome-in-iframe')` 将无法工作 — 请在 ESM 项目中使用或配置打包器将其作为 ESM 外部依赖处理。
56
+
57
+ ### 包元数据
58
+
59
+ - 添加了 `engines`、`sideEffects: false`。
60
+ - `prepublishOnly` 依次运行 `typecheck`、`test` 和 `build`。
61
+ - `lint` 同时覆盖 `src` 和 `test` 目录。
62
+ - 将 `LICENSE` 和 `CHANGELOG.md` 打包到发布 tarball 中。
63
+ - `Endpoint.getContext` 标记为 `@internal` — 仅保留用于测试,不应在应用代码中使用。
64
+
65
+ ## 1.0.1
66
+
67
+ - 升级 `nanoid` 至 `^5.1.11`。
68
+ - 将 README 中的示例扩展 ID 从 `chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb` 更新为 `chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc`。
69
+
70
+ ## 1.0.0
71
+
72
+ 首次公开发布。
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 GumerLee
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -8,6 +8,20 @@ Bridge Chrome extension APIs from a Chrome extension page into an embedded ifram
8
8
 
9
9
  This is useful when an extension page such as `options.html`, `popup.html`, or another `chrome-extension://...` page embeds an iframe, but the iframe itself cannot directly access Chrome extension APIs.
10
10
 
11
+ ## Module Format
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">`).
14
+
15
+ `require('chrome-in-iframe')` will not work.
16
+
17
+ ```json
18
+ {
19
+ "type": "module"
20
+ }
21
+ ```
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.
24
+
11
25
  ## Install
12
26
 
13
27
  ```bash
@@ -133,9 +147,9 @@ window.addEventListener('beforeunload', () => {
133
147
 
134
148
  ## Security Options
135
149
 
136
- `targetOrigin` and `allowedOrigin` are optional. If omitted, messages are sent with `'*'` and incoming messages are only checked against the expected iframe or parent window source (via `MessageEvent.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 additionally checked against the expected window source.
137
151
 
138
- > **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** can send forged `postMessage` messages and impersonate Chrome API calls. In that case, always set `allowedOrigin` to the expected origin.
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.
139
153
 
140
154
  For production, pass origins when you know them.
141
155
 
@@ -155,11 +169,22 @@ exposeChromeInIframe({
155
169
  ```ts
156
170
  const { chrome } = connectChromeInIframe({
157
171
  key: 'my-extension-bridge',
158
- targetOrigin: 'chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb',
159
- allowedOrigin: 'chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb',
172
+ targetOrigin: 'chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc',
173
+ allowedOrigin: 'chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc',
160
174
  });
161
175
  ```
162
176
 
177
+ ## Data Types Across the Bridge
178
+
179
+ The bridge serializes `undefined`, `null`, all primitive types (including `bigint`, `NaN`, `Infinity`, `-0`), plain objects (string- and symbol-keyed), arrays, functions, `Error` (preserves `name`, `stack`, and `cause`), `Date`, `RegExp`, `Map`, and `Set`. Circular references throw.
180
+
181
+ A few inherent limitations of any RPC-style bridge:
182
+
183
+ - **Object identity is not preserved.** A `Date`, `Map`, `Set`, or plain object that crosses the bridge is reconstructed on the other side. `===` between the original and the round-tripped value is `false`.
184
+ - **Map / Set keyed on objects loses lookup.** If you put a `Date` (or any object) into a `Map` and send the `Map` across, `map.get(originalDate)` on the receiving side does **not** find it — the deserialized `Map` holds a different `Date` instance. Key on strings or numbers when the `Map` is meant to travel.
185
+ - **Non-global symbols cannot cross.** Use `Symbol.for(...)` or a well-known symbol; ad-hoc `Symbol('local')` throws on serialization.
186
+ - **Class prototypes are erased.** A `class Foo { ... }` instance arrives as a plain object with the same enumerable properties. `instanceof Foo` is `false`.
187
+
163
188
  ## Reading Properties
164
189
 
165
190
  Chrome API methods can be called directly:
@@ -168,13 +193,54 @@ Chrome API methods can be called directly:
168
193
  const tabs = await chrome.tabs.query({ active: true });
169
194
  ```
170
195
 
171
- Readable properties should be read through `get()` or `$get()`:
196
+ Readable properties — anything that is not a function call — should be read through `get()` (returned from `connectChromeInIframe`) or `$get()` (available on every proxy node).
197
+
198
+ ### Path Forms
199
+
200
+ `$get` / `get` accept three equivalent shapes for the path:
172
201
 
173
202
  ```ts
174
- const extensionId = await get<string>('runtime.id');
175
- const platformOs = await chrome.runtime.$get<string>('PlatformOs');
203
+ // 1. Array form (recommended) — each segment is taken verbatim
204
+ await get<string>(['runtime', 'id']);
205
+
206
+ // 2. Multiple string arguments — each argument is one segment
207
+ await get<string>('runtime', 'id');
208
+
209
+ // 3. Single dotted string — split on `.`
210
+ await get<string>('runtime.id');
176
211
  ```
177
212
 
213
+ You can also start from a nested proxy node:
214
+
215
+ ```ts
216
+ await chrome.runtime.$get<string>('id');
217
+ await chrome.runtime.$get<string>(['id']);
218
+ ```
219
+
220
+ Symbols are allowed as path keys (must be `Symbol.for(...)` or a well-known symbol so identity survives the wire):
221
+
222
+ ```ts
223
+ const queryKey = Symbol.for('queryBySymbol');
224
+ await get(queryKey);
225
+ ```
226
+
227
+ ### Dotted Keys: Use the Array Form
228
+
229
+ The dotted-string form is convenient, but it **splits on every `.` it finds**, so it cannot address keys that contain a dot — for example `chrome.storage.local`'s users sometimes store entries under names like `'user.email'` or `'flag.v2'`. To address those safely, use the array form (or pass each segment as its own argument):
230
+
231
+ ```ts
232
+ // ❌ Wrong — the dotted form would try to read `local.user.email`
233
+ await get('storage.local.user.email');
234
+
235
+ // ✅ Correct — the array form treats `user.email` as a single key
236
+ await get(['storage', 'local', 'user.email']);
237
+
238
+ // ✅ Also correct — multiple arguments are kept verbatim
239
+ await get('storage', 'local', 'user.email');
240
+ ```
241
+
242
+ Rule of thumb: **prefer the array form whenever the path is dynamic or comes from data you don't fully control.** Reserve the dotted-string form for short, hand-written, static lookups.
243
+
178
244
  ## API
179
245
 
180
246
  ### `exposeChromeInIframe(options)`
@@ -188,6 +254,11 @@ const bridge = exposeChromeInIframe({
188
254
  targetOrigin,
189
255
  allowedOrigin,
190
256
  chromeApi,
257
+ timeout,
258
+ functionCacheMax,
259
+ functionCacheTtl,
260
+ remoteCallbackCacheMax,
261
+ remoteCallbackCacheTtl,
191
262
  });
192
263
  ```
193
264
 
@@ -195,15 +266,18 @@ Options:
195
266
 
196
267
  - `iframe`: the iframe element to bridge.
197
268
  - `key`: optional shared bridge key. Use the same key on both sides.
198
- - `targetOrigin`: optional origin used by `postMessage`.
269
+ - `targetOrigin`: optional origin used by `postMessage`. Falls back to a value resolved from `iframe.src`.
199
270
  - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages.
200
271
  - `chromeApi`: optional custom Chrome-like object. Defaults to `globalThis.chrome`.
272
+ - `timeout`: optional per-call timeout in ms (default `30000`).
273
+ - `functionCacheMax` / `functionCacheTtl`: tune the LRU that stores non-persistent callbacks the local side has sent.
274
+ - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`: tune the LRU that stores callbacks the remote side has handed to us.
201
275
 
202
276
  Returns:
203
277
 
204
278
  - `proxy`: the bridged Chrome API proxy.
205
- - `get(path)`: read a property from the bridged Chrome API.
206
- - `destroy()`: remove message listeners and clear bridge state.
279
+ - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
280
+ - `destroy()`: remove message listeners, clear bridge state, and tell the remote peer to release any callbacks it is holding on our behalf.
207
281
 
208
282
  ### `connectChromeInIframe(options)`
209
283
 
@@ -214,6 +288,11 @@ const { chrome, get, destroy } = connectChromeInIframe({
214
288
  key,
215
289
  targetOrigin,
216
290
  allowedOrigin,
291
+ timeout,
292
+ functionCacheMax,
293
+ functionCacheTtl,
294
+ remoteCallbackCacheMax,
295
+ remoteCallbackCacheTtl,
217
296
  });
218
297
  ```
219
298
 
@@ -221,6 +300,5 @@ Returns:
221
300
 
222
301
  - `chrome`: a proxy for the extension page's Chrome API.
223
302
  - `proxy`: the same object as `chrome`.
224
- - `get(path)`: read a property from the bridged Chrome API.
225
- - `destroy()`: remove message listeners and clear bridge state.
226
-
303
+ - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
304
+ - `destroy()`: remove message listeners, clear bridge state, and notify the remote peer.
package/README.zh-CN.md CHANGED
@@ -8,6 +8,20 @@
8
8
 
9
9
  当扩展页面(如 `options.html`、`popup.html` 或其他 `chrome-extension://...` 页面)嵌入了 iframe,但 iframe 本身无法直接访问 Chrome 扩展 API 时,这个库非常有用。
10
10
 
11
+ ## 模块格式
12
+
13
+ **本包只提供 ESM。** 发布产物是单个 ECMAScript 模块,没有 CommonJS 入口。请在 ESM 项目中使用(Vite、Rollup、webpack 5+、Parcel 2+、其他现代打包器,或浏览器原生 `<script type="module">`)。
14
+
15
+ `require('chrome-in-iframe')` 无法工作。
16
+
17
+ ```json
18
+ {
19
+ "type": "module"
20
+ }
21
+ ```
22
+
23
+ 如果你的构建工具仍然产出 CommonJS,请将 `chrome-in-iframe` 配置为 ESM external,或将消费方项目迁移到 ESM。
24
+
11
25
  ## 安装
12
26
 
13
27
  ```bash
@@ -133,11 +147,11 @@ window.addEventListener('beforeunload', () => {
133
147
 
134
148
  ## 安全选项
135
149
 
136
- `targetOrigin` 和 `allowedOrigin` 是可选的。如果省略,消息将使用 `'*'` 发送,传入的消息仅通过 `MessageEvent.source` 与预期的 iframe 或父窗口进行校验。
150
+ `targetOrigin` 和 `allowedOrigin` 是可选的。当不传 `targetOrigin` 时,库会自动从 `iframe.src` / `contentWindow.location.origin`(扩展侧)或 `document.referrer` / `location.ancestorOrigins`(iframe 侧)推导出 origin;只有在以上来源都不可用时才回退到 `'*'`。传入的消息还会通过预期的 window source 进行校验。
137
151
 
138
- > **警告**:当两侧都是同一 `chrome-extension://` origin 下的 Chrome 扩展页面时,默认行为是安全的。但如果 iframe 加载的是**第三方页面**或来自**不同 origin** 的页面,不设置 `allowedOrigin` 意味着**任何窗口**都可以发送伪造的 `postMessage` 消息来冒充 Chrome API 调用。在这种情况下,务必将 `allowedOrigin` 设置为预期的 origin。
152
+ > **警告**:当两侧都是同一 `chrome-extension://` origin 下的 Chrome 扩展页面时,默认行为是安全的。但如果 iframe 加载的是**第三方页面**或来自**不同 origin** 的页面,不设置 `allowedOrigin` 意味着**任何能向页面发送消息的窗口**都可以伪造 `postMessage` 来冒充 Chrome API 调用。在这种情况下,务必将 `allowedOrigin` 设置为预期的 origin。
139
153
 
140
- 在生产环境中,如果知道具体来源,建议传入 origin。
154
+ 在生产环境中,如果知道具体来源,建议显式传入 origin。
141
155
 
142
156
  ### 扩展页面
143
157
 
@@ -155,11 +169,22 @@ exposeChromeInIframe({
155
169
  ```ts
156
170
  const { chrome } = connectChromeInIframe({
157
171
  key: 'my-extension-bridge',
158
- targetOrigin: 'chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb',
159
- allowedOrigin: 'chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb',
172
+ targetOrigin: 'chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc',
173
+ allowedOrigin: 'chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc',
160
174
  });
161
175
  ```
162
176
 
177
+ ## 跨桥的数据类型
178
+
179
+ 桥接层支持 `undefined`、`null`、所有原始类型(含 `bigint`、`NaN`、`Infinity`、`-0`)、普通对象(字符串键和 symbol 键都行)、数组、函数、`Error`(保留 `name`、`stack`、`cause`)、`Date`、`RegExp`、`Map`、`Set`。循环引用会抛错。
180
+
181
+ 任何 RPC 桥接都有的固有限制:
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`。
187
+
163
188
  ## 读取属性
164
189
 
165
190
  Chrome API 方法可以直接调用:
@@ -168,13 +193,54 @@ Chrome API 方法可以直接调用:
168
193
  const tabs = await chrome.tabs.query({ active: true });
169
194
  ```
170
195
 
171
- 可读属性应通过 `get()` `$get()` 读取:
196
+ 非函数的可读属性应通过 `get()`(由 `connectChromeInIframe` 返回)或代理上任意节点的 `$get()` 读取。
197
+
198
+ ### 路径形式
199
+
200
+ `$get` / `get` 支持三种等价的路径写法:
172
201
 
173
202
  ```ts
174
- const extensionId = await get<string>('runtime.id');
175
- const platformOs = await chrome.runtime.$get<string>('PlatformOs');
203
+ // 1. 数组形式(推荐)—— 每个元素都被视为单独一段
204
+ await get<string>(['runtime', 'id']);
205
+
206
+ // 2. 多参数字符串 —— 每个参数都是单独一段
207
+ await get<string>('runtime', 'id');
208
+
209
+ // 3. 单个带 `.` 的字符串 —— 按 `.` 自动拆分
210
+ await get<string>('runtime.id');
211
+ ```
212
+
213
+ 也可以从代理的中间节点开始:
214
+
215
+ ```ts
216
+ await chrome.runtime.$get<string>('id');
217
+ await chrome.runtime.$get<string>(['id']);
218
+ ```
219
+
220
+ Symbol 也可以作为路径片段(必须是 `Symbol.for(...)` 或 well-known symbol,以便在序列化后仍能保持身份):
221
+
222
+ ```ts
223
+ const queryKey = Symbol.for('queryBySymbol');
224
+ await get(queryKey);
176
225
  ```
177
226
 
227
+ ### 含 `.` 的键:请用数组形式
228
+
229
+ 字符串带 `.` 的写法很方便,但会**对每一个 `.` 都做拆分**,所以无法访问 key 本身就包含 `.` 的属性 —— 例如 `chrome.storage.local` 的使用者经常会存 `'user.email'`、`'flag.v2'` 这类条目。要安全访问它们,必须改用数组形式(或把每段当作独立参数传):
230
+
231
+ ```ts
232
+ // ❌ 错误 —— 字符串形式会把它当作 `local → user → email` 三段
233
+ await get('storage.local.user.email');
234
+
235
+ // ✅ 正确 —— 数组形式把 `user.email` 当作一段
236
+ await get(['storage', 'local', 'user.email']);
237
+
238
+ // ✅ 也正确 —— 多参数形式同样保留原样
239
+ await get('storage', 'local', 'user.email');
240
+ ```
241
+
242
+ 经验法则:**路径只要是动态的,或者来自不完全可控的数据源,就用数组形式。** 字符串带 `.` 的形式留给短小、手写、静态的查询。
243
+
178
244
  ## API
179
245
 
180
246
  ### `exposeChromeInIframe(options)`
@@ -188,22 +254,30 @@ const bridge = exposeChromeInIframe({
188
254
  targetOrigin,
189
255
  allowedOrigin,
190
256
  chromeApi,
257
+ timeout,
258
+ functionCacheMax,
259
+ functionCacheTtl,
260
+ remoteCallbackCacheMax,
261
+ remoteCallbackCacheTtl,
191
262
  });
192
263
  ```
193
264
 
194
265
  选项:
195
266
 
196
267
  - `iframe`:要桥接的 iframe 元素。
197
- - `key`:可选的共享桥接密钥。两侧使用相同的 key。
198
- - `targetOrigin`:可选,`postMessage` 使用的 origin
268
+ - `key`:可选的共享桥接 key。两侧使用相同的 key
269
+ - `targetOrigin`:可选,`postMessage` 使用的 origin。不传时由 `iframe.src` 推导。
199
270
  - `allowedOrigin`:可选,用于过滤传入消息的 origin、origin 列表或判断函数。
200
271
  - `chromeApi`:可选,自定义的 Chrome API 对象。默认为 `globalThis.chrome`。
272
+ - `timeout`:可选,单次调用的超时时长(毫秒,默认 `30000`)。
273
+ - `functionCacheMax` / `functionCacheTtl`:调整本地存储的非持久 callback LRU。
274
+ - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`:调整对端交给我们的 callback LRU。
201
275
 
202
276
  返回值:
203
277
 
204
278
  - `proxy`:桥接后的 Chrome API 代理。
205
- - `get(path)`:从桥接的 Chrome API 中读取属性。
206
- - `destroy()`:移除消息监听器并清除桥接状态。
279
+ - `get(path)`:从桥接的 Chrome API 中读取属性。详见 [路径形式](#路径形式)。
280
+ - `destroy()`:移除消息监听器,清理桥接状态,并通知对端释放它为我们持有的 callback。
207
281
 
208
282
  ### `connectChromeInIframe(options)`
209
283
 
@@ -214,6 +288,11 @@ const { chrome, get, destroy } = connectChromeInIframe({
214
288
  key,
215
289
  targetOrigin,
216
290
  allowedOrigin,
291
+ timeout,
292
+ functionCacheMax,
293
+ functionCacheTtl,
294
+ remoteCallbackCacheMax,
295
+ remoteCallbackCacheTtl,
217
296
  });
218
297
  ```
219
298
 
@@ -221,5 +300,5 @@ const { chrome, get, destroy } = connectChromeInIframe({
221
300
 
222
301
  - `chrome`:扩展页面 Chrome API 的代理。
223
302
  - `proxy`:与 `chrome` 相同的对象。
224
- - `get(path)`:从桥接的 Chrome API 中读取属性。
225
- - `destroy()`:移除消息监听器并清除桥接状态。
303
+ - `get(path)`:从桥接的 Chrome API 中读取属性。详见 [路径形式](#路径形式)。
304
+ - `destroy()`:移除消息监听器,清理桥接状态,并通知对端。
package/dist/api.d.ts CHANGED
@@ -11,6 +11,10 @@ export interface ConnectionOptions {
11
11
  timeout?: number;
12
12
  targetOrigin?: string;
13
13
  allowedOrigin?: AllowedOrigin;
14
+ functionCacheMax?: number;
15
+ functionCacheTtl?: number;
16
+ remoteCallbackCacheMax?: number;
17
+ remoteCallbackCacheTtl?: number;
14
18
  }
15
19
  export interface SetupInMainWindowOptions<T = unknown> extends ConnectionOptions {
16
20
  iframe: HTMLIFrameElement;
@@ -1,3 +1,3 @@
1
1
  import type { ProcessorRegistry } from '../processor/registry';
2
2
  import type { ClientContext, MessageChannel, MessagePoster } from './types';
3
- export declare function createMessageChannel(poster: MessagePoster, key: string, context: ClientContext, processorRegistry: ProcessorRegistry): MessageChannel;
3
+ export declare function createMessageChannel(poster: MessagePoster, key: string, instanceId: string, context: ClientContext, processorRegistry: ProcessorRegistry): MessageChannel;
@@ -1,2 +1,4 @@
1
1
  import type { Callable, CallbackCacheOptions, MessageValue } from './types';
2
- export declare function deserialize(arg: MessageValue, generateCallback: (id: string, args: MessageValue[], options?: CallbackCacheOptions) => MessageValue, getRemoteCallback?: (id: string, invoke: (args: MessageValue[]) => void, options?: CallbackCacheOptions) => Callable, options?: CallbackCacheOptions): MessageValue;
2
+ export type GenerateCallbackFn = (id: string, args: MessageValue[], options?: CallbackCacheOptions) => MessageValue;
3
+ export type GetRemoteCallbackFn = (id: string, invoke: (args: MessageValue[]) => MessageValue, options?: CallbackCacheOptions) => Callable;
4
+ export declare function deserialize(arg: MessageValue, generateCallback: GenerateCallbackFn, getRemoteCallback?: GetRemoteCallbackFn): MessageValue;
@@ -1,3 +1,4 @@
1
- import type { MessageValue, PathKey } from './types';
1
+ import type { PathKey } from './types';
2
2
  export declare function isListenerRegistrationPath(path: PathKey[]): boolean;
3
- export declare function isLikelyListenerPath(path: PathKey[], value?: MessageValue): boolean;
3
+ export declare function isListenerRemovalPath(path: PathKey[]): boolean;
4
+ export declare function isLikelyListenerPath(path: PathKey[]): boolean;
@@ -1,2 +1,2 @@
1
1
  import type { MessagePoster, MessageSender } from './types';
2
- export declare function createMessageSender(poster: MessagePoster, key: string): MessageSender;
2
+ export declare function createMessageSender(poster: MessagePoster, key: string, instanceId: string): MessageSender;
@@ -1,2 +1,4 @@
1
1
  import type { Callable, CallbackCacheOptions, MessageValue } from './types';
2
- export declare function serialize(arg: MessageValue, registerFunction: (fn: Callable, options?: CallbackCacheOptions) => string, options?: CallbackCacheOptions): MessageValue;
2
+ export declare const TYPE_KEY = "$cii$";
3
+ export type RegisterFunctionFn = (fn: Callable, thisArg: MessageValue, options?: CallbackCacheOptions) => string;
4
+ export declare function serialize(arg: MessageValue, registerFunction: RegisterFunctionFn, options?: CallbackCacheOptions): MessageValue;