chrome-in-iframe 1.0.1 → 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 +140 -0
- package/CHANGELOG.zh-CN.md +72 -0
- package/LICENSE +15 -0
- package/README.md +89 -11
- package/README.zh-CN.md +91 -12
- package/dist/api.d.ts +4 -0
- package/dist/channel/channel.d.ts +1 -1
- package/dist/channel/deserializer.d.ts +3 -1
- package/dist/channel/listener.d.ts +3 -2
- package/dist/channel/sender.d.ts +1 -1
- package/dist/channel/serializer.d.ts +3 -1
- package/dist/channel/types.d.ts +25 -7
- package/dist/client/context.d.ts +5 -1
- package/dist/client/endpoint.d.ts +3 -1
- package/dist/index.js +772 -237
- package/dist/processor/accessProperty.d.ts +3 -3
- package/dist/processor/invoke.d.ts +3 -3
- package/dist/processor/invokeCallback.d.ts +3 -3
- package/dist/processor/lifecycle.d.ts +3 -0
- package/package.json +14 -5
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.
|
|
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
|
|
|
@@ -160,6 +174,17 @@ const { chrome } = connectChromeInIframe({
|
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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`
|
|
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`
|
|
152
|
+
> **警告**:当两侧都是同一 `chrome-extension://` origin 下的 Chrome 扩展页面时,默认行为是安全的。但如果 iframe 加载的是**第三方页面**或来自**不同 origin** 的页面,不设置 `allowedOrigin` 意味着**任何能向页面发送消息的窗口**都可以伪造 `postMessage` 来冒充 Chrome API 调用。在这种情况下,务必将 `allowedOrigin` 设置为预期的 origin。
|
|
139
153
|
|
|
140
|
-
|
|
154
|
+
在生产环境中,如果知道具体来源,建议显式传入 origin。
|
|
141
155
|
|
|
142
156
|
### 扩展页面
|
|
143
157
|
|
|
@@ -160,6 +174,17 @@ const { chrome } = connectChromeInIframe({
|
|
|
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
|
-
|
|
196
|
+
非函数的可读属性应通过 `get()`(由 `connectChromeInIframe` 返回)或代理上任意节点的 `$get()` 读取。
|
|
197
|
+
|
|
198
|
+
### 路径形式
|
|
199
|
+
|
|
200
|
+
`$get` / `get` 支持三种等价的路径写法:
|
|
172
201
|
|
|
173
202
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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 {
|
|
1
|
+
import type { PathKey } from './types';
|
|
2
2
|
export declare function isListenerRegistrationPath(path: PathKey[]): boolean;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function isListenerRemovalPath(path: PathKey[]): boolean;
|
|
4
|
+
export declare function isLikelyListenerPath(path: PathKey[]): boolean;
|
package/dist/channel/sender.d.ts
CHANGED
|
@@ -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
|
|
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;
|