Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
c0444dc
🔒 harden MAIN-world GM RPC capability binding
cyfung1031 Sep 16, 2026
a6e69ed
🐛 allow async GM XHR page RPC alias
cyfung1031 Sep 16, 2026
ef945e8
🔒 harden userscript invocation against page substitution
cyfung1031 Sep 16, 2026
1eb1839
🔒 isolate USER_SCRIPT GM transport and binding rotation
cyfung1031 Sep 17, 2026
3037623
🐛 route USER_SCRIPT GM calls to service worker
cyfung1031 Sep 17, 2026
25714ff
🧹 format USER_SCRIPT transport regression test
cyfung1031 Sep 17, 2026
a46b042
🔒 harden page and USER_SCRIPT execution bindings
cyfung1031 Sep 17, 2026
d65bcf3
🔒 secure USER_SCRIPT bootstrap and page capability checks
cyfung1031 Sep 17, 2026
e1ccf07
🐛 preserve GM API registry enumeration
cyfung1031 Sep 17, 2026
9f98995
🐛 make GM API registry merge-compatible
cyfung1031 Sep 17, 2026
9868594
⚡ reduce native reflection overhead
cyfung1031 Sep 17, 2026
f49053a
⚡ reduce content callback overhead
cyfung1031 Sep 17, 2026
d56b7a8
Merge remote-tracking branch 'origin/main' into codex/main-world-secu…
cyfung1031 Sep 17, 2026
47c2704
⚡ avoid receiver binding for pure agent APIs
cyfung1031 Sep 17, 2026
8bcad36
⚡ capture native collection methods on subclasses
cyfung1031 Sep 17, 2026
f2aad91
⚡ remove GM API receiver binding
cyfung1031 Sep 17, 2026
f13f7e2
⚡️ reduce runtime function overhead
cyfung1031 Sep 18, 2026
edc3ab3
⚡️ remove redundant native collection factories
cyfung1031 Sep 18, 2026
b5007df
⚡️ simplify native set initialization
cyfung1031 Sep 18, 2026
4d194e1
📄 补充跨世界安全边界维护注释
cyfung1031 Sep 18, 2026
9d0de58
🔒 separate asynchronous page RPC transport
cyfung1031 Sep 18, 2026
3448728
🔒 hide GM broker state behind script facade
cyfung1031 Sep 18, 2026
6a014bf
🔒 reject executable values at GM clone boundaries
cyfung1031 Sep 18, 2026
eb66888
🐛 settle GM XHR aborts without callbacks
cyfung1031 Sep 18, 2026
1d0dd85
🐛 continue page script loading after early reconciliation
cyfung1031 Sep 18, 2026
7780934
🔒 clone DOM bridge payloads before validation
cyfung1031 Sep 18, 2026
2993bbc
🔒 validate Blob page RPC payloads
cyfung1031 Sep 18, 2026
9c0650d
🔒 reject executable GM values
cyfung1031 Sep 18, 2026
398c884
🔒 reject non-cloneable GM values
cyfung1031 Sep 18, 2026
06bbf29
🔒 harden page RPC intrinsic boundaries
cyfung1031 Sep 18, 2026
4a4a127
🐛 preserve zero-valued tab identities
cyfung1031 Sep 18, 2026
a5a9b84
🐛 delete falsy GM values consistently
cyfung1031 Sep 18, 2026
340fea9
🔒 avoid accessor execution in GM_setValues
cyfung1031 Sep 18, 2026
884642a
🔒 preserve none grant capability boundary
cyfung1031 Sep 18, 2026
3d45436
🔒 capture grant alias intrinsics
cyfung1031 Sep 18, 2026
764a550
🔒 isolate grant capability map lookups
cyfung1031 Sep 18, 2026
525b2ed
🔒 harden capability collection construction
cyfung1031 Sep 18, 2026
c37446a
🔒 seal page RPC collection iteration
cyfung1031 Sep 18, 2026
18255a0
🐛 honor early GM XHR aborts
cyfung1031 Sep 18, 2026
ad61560
🐛 settle GM XHR connection failures
cyfung1031 Sep 18, 2026
68f4d3c
🐛 settle GM XHR setup errors
cyfung1031 Sep 18, 2026
f4edd72
🔒 discard stale page load bindings
cyfung1031 Sep 18, 2026
842d5ee
🔒 harden GM value transport collection writes
cyfung1031 Sep 18, 2026
fa26a1b
🔒 bind page execution handles to navigation URL
cyfung1031 Sep 18, 2026
86193c0
🔒 hide CAT conversation state behind private fields
cyfung1031 Sep 18, 2026
0bc23a6
🔒 close GM XHR connections on setup failure
cyfung1031 Sep 18, 2026
35b6766
🔒 avoid accessor execution in GM DOM attributes
cyfung1031 Sep 18, 2026
cb49bf8
🔒 copy GM menu options without accessors
cyfung1031 Sep 18, 2026
ef3adf5
🔒 copy GM XHR headers without accessors
cyfung1031 Sep 18, 2026
5acba1b
🔒 copy GM tab options without accessors
cyfung1031 Sep 18, 2026
13d6645
🔒 copy notification details without accessors
cyfung1031 Sep 18, 2026
6bf76fd
⚡️ reduce USER_SCRIPT callback dispatch scans
cyfung1031 Sep 18, 2026
714008e
🔒 prune revoked USER_SCRIPT connections
cyfung1031 Sep 18, 2026
63b01b3
🔒 harden USER_SCRIPT reconnect sessions
cyfung1031 Sep 18, 2026
3a8c561
🔒 validate inject page bootstrap DTOs
cyfung1031 Sep 18, 2026
cf9ee7b
🔒 validate inject runtime callback DTOs
cyfung1031 Sep 18, 2026
d2b2d84
🔒 avoid duplicate USER_SCRIPT bootstrap execution
cyfung1031 Sep 18, 2026
a64a1cb
🔒 route MAIN privileged traffic over native channel
cyfung1031 Sep 18, 2026
281e449
🔒 redact early-start preload values
cyfung1031 Sep 18, 2026
50a08b2
🔒 settle XHR errors without broker loadend
cyfung1031 Sep 18, 2026
b84ccaf
🔒 isolate protected GM facade keys
cyfung1031 Sep 18, 2026
cfb698c
🔒 keep service worker bridge bundle DOM-free
cyfung1031 Sep 18, 2026
8df243d
🔒 validate USER_SCRIPT bridge DTOs
cyfung1031 Sep 18, 2026
beb742c
🔒 bind CAT tasks to script owners
cyfung1031 Sep 18, 2026
e2738c9
🔒 bind CAT conversations to script owners
cyfung1031 Sep 18, 2026
283edd2
🔒 bind USER_SCRIPT tokens to page URLs
cyfung1031 Sep 18, 2026
c8516c9
🔒 bind early-start metadata to injected wrappers
cyfung1031 Sep 18, 2026
20d39af
🔒 restore early-start manifest execution
cyfung1031 Sep 18, 2026
3372fbe
🔒 retire stale page bindings on navigation
cyfung1031 Sep 18, 2026
f371261
🔒 bind CAT agent service identities
cyfung1031 Sep 18, 2026
3ab6b2d
🔒 target private callbacks to frame zero
cyfung1031 Sep 18, 2026
2f5b1fb
🔒 bind DOM monitors to script owners
cyfung1031 Sep 18, 2026
1f769a6
🔒 gate MAIN page bootstrap transport
cyfung1031 Sep 18, 2026
2f8dcd6
🔒 preserve USER_SCRIPT native fallback
cyfung1031 Sep 18, 2026
901016d
🔒 queue USER_SCRIPT value updates across reconnects
cyfung1031 Sep 18, 2026
643af56
🔒 protect GM value stores from prototype keys
cyfung1031 Sep 18, 2026
8f4cf28
🔒 reject hostile page message envelopes
cyfung1031 Sep 18, 2026
5b25310
🔒 validate sandbox message envelopes
cyfung1031 Sep 18, 2026
898fcc4
🔒 validate custom event envelopes
cyfung1031 Sep 18, 2026
35ce581
🔒 guard server action dispatch inputs
cyfung1031 Sep 18, 2026
276196c
🔒 inspect collection payload entries before cloning
cyfung1031 Sep 18, 2026
c783a7a
🔒 validate collection RPC payloads without accessors
cyfung1031 Sep 18, 2026
671c445
🔒 remove live document references from CAT fetch
cyfung1031 Sep 18, 2026
284b968
🔒 bind early-start wrappers to document URLs
cyfung1031 Sep 18, 2026
3693b58
🔒 reject page RPC replay window overflow
cyfung1031 Sep 18, 2026
9cf16de
🔒 close service worker page RPC replay window
cyfung1031 Sep 18, 2026
9dd323e
🔒 validate page RPC identity before replay state
cyfung1031 Sep 18, 2026
2acb238
🔒 harden GM value result records
cyfung1031 Sep 18, 2026
39b695d
🔒 restrict script attachment reads
cyfung1031 Sep 18, 2026
c46dfe1
🔒 keep MAIN script payload off page bridge
cyfung1031 Sep 18, 2026
deea129
🔒 isolate service worker value records
cyfung1031 Sep 18, 2026
71022c7
🔒 serialize CDP monitor transitions
cyfung1031 Sep 18, 2026
8528137
🔒 allow long-lived page RPC bindings
cyfung1031 Sep 18, 2026
96c577c
🔒 preserve early wrappers across same-document navigation
cyfung1031 Sep 18, 2026
6ee8ba4
🔒 require a native MAIN page-load handshake
cyfung1031 Sep 18, 2026
babadbc
✅ bound E2E waits and replay invariant tests
cyfung1031 Sep 18, 2026
0541cdb
🔧 restore PR E2E timeout settings
cyfung1031 Sep 18, 2026
3217aca
🔒 route MAIN bootstrap over extension transport
cyfung1031 Sep 18, 2026
dbad992
🔒 restore MAIN bootstrap page load
cyfung1031 Sep 19, 2026
0057ee8
⚡️ 优化跨上下文消息解析热路径
cyfung1031 Sep 19, 2026
cf6cf21
⚡️ 精简跨上下文消息体校验
cyfung1031 Sep 19, 2026
2d8a75a
⚡️ reuse PageMessage connection sender
cyfung1031 Sep 19, 2026
5b07c7e
⚡️ 优化 structuredClone 调用
cyfung1031 Sep 19, 2026
dcd220c
📚 更新 Agent 与消息传输架构文档
cyfung1031 Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ Service Worker (src/service_worker.ts)
> SW → Offscreen uses `ServiceWorkerMessageSend` (`clients.matchAll()` + `postMessage`) on Chrome and
> `EventPageOffscreenManager` on Firefox MV3; Offscreen replies to SW over `ExtensionMessage`. `WindowMessage`
> is the Offscreen ↔ Sandbox channel.
>
> USER_SCRIPT content and MAIN inject runtimes normally use native extension channels directly to the SW.
> The `scripting` bundle is a document-start extension content script registered per matching frame. It runs a
> page-bridge runtime and is a supporting per-document helper rather than a separate service/background context in
> this five-context model. Those bridges carry the content bootstrap handoff, MAIN bootstrap/fallback and runtime update packets,
> synchronous DOM handles, and the whitelisted `external.Scriptcat` API. When MAIN GM RPC falls back through
> `PageMessage`, the scripting runtime validates its execution handle and grant before forwarding it to the SW.

- **Service Worker** — central hub for script CRUD, Chrome APIs, permission verification, resource caching, and message routing.
- **Content** — bridges SW and inject script.
Expand All @@ -244,9 +251,13 @@ Sandbox.

### Message Passing (`packages/message/`)

`ExtensionMessage` (chrome.runtime — SW ↔ Content / Inject / Offscreen), `WindowMessage` (postMessage — Offscreen ↔
Sandbox), `ServiceWorkerMessageSend` (`clients.matchAll()` + `postMessage` — SW → Offscreen on Chrome),
`CustomEventMessage` (CustomEvent — Content ↔ Inject), and `MessageQueue` (cross-context broadcast).
`ExtensionMessage` (chrome.runtime — SW ↔ Content / Inject / Offscreen), `PageMessage` (`window.postMessage` —
scripting ↔ Inject page bridge, including validated MAIN RPC fallback), `CustomEventMessage` (CustomEvent —
bootstrap handoff and DOM handles),
`WindowMessage` (`postMessage` — Offscreen ↔ Sandbox), `ServiceWorkerMessageSend` (`clients.matchAll()` +
`postMessage` — SW → Offscreen on Chrome), and `MessageQueue` (cross-context broadcast). Page-visible bridges do
not establish an authenticated extension origin; MAIN requests relayed through `PageMessage` must pass the
`PageRpcRegistry` checks before forwarding.

### Service & Data Layers

Expand Down
19 changes: 15 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ Three ideas explain almost everything in the codebase:
inject, and sandbox don't hold a MessageQueue instance.
```

The diagram compresses the page-facing routes: USER_SCRIPT content and MAIN inject runtimes also connect directly
to the Service Worker through native extension channels on the preferred path. The `scripting` bundle is a
document-start extension content script registered per matching frame; it runs a page-bridge runtime and is a
supporting per-document helper rather than a separate service/background context in this five-context model.
`CustomEventMessage` carries the content bootstrap
handoff and synchronous DOM handles; `PageMessage` carries MAIN bootstrap/fallback traffic, runtime event/value
updates, and the whitelisted `external.Scriptcat` API. When MAIN GM RPC uses the page bridge fallback, the scripting
runtime validates the execution handle and grant before forwarding it to the Service Worker; the page bridge
itself is not an authenticated extension origin.

---

## The Five Contexts (Process Model)
Expand All @@ -86,8 +96,8 @@ Each context is a separate bundle (see [Build pipeline & manifest](./references/
| Context | Entry | Realm / capabilities | Bootstraps |
|---|---|---|---|
| **Service Worker** | [`src/service_worker.ts`](../src/service_worker.ts) | No DOM. Owns `chrome.*` privileged APIs, storage, permissions, routing. | `ExtensionMessage(true)` → `Server("serviceWorker")` + `MessageQueue` → `ServiceWorkerManager` |
| **Content** | [`src/content.ts`](../src/content.ts) | Isolated content-script world. Bridges SW and the page. | `CustomEventMessage` channel to inject + `Server("content")` → `ScriptRuntime` |
| **Inject** | [`src/inject.ts`](../src/inject.ts) | Page (`MAIN`) world. Has `unsafeWindow`; runs page userscripts. | `CustomEventMessage` to content + `Server("inject")` |
| **Content** | [`src/content.ts`](../src/content.ts) | `USER_SCRIPT` world. Receives a document bootstrap token through the page-side bridge, then uses a native extension channel for script loading, GM RPC, value updates, and callbacks. Dedicated USER_SCRIPT listeners are used when available; otherwise the regular port is token-bound. | `ExtensionMessage` + native callback port → `Server("content")` → `ScriptRuntime`; `CustomEventMessage` for bootstrap handoff and DOM handles |
| **Inject** | [`src/inject.ts`](../src/inject.ts) | Page (`MAIN`) world. Has `unsafeWindow`; runs page userscripts. | Native extension port for the preferred GM RPC path; `PageMessage` for bootstrap/fallback, whitelisted external API, and validated GM RPC fallback; `CustomEventMessage` for synchronous DOM handles |
| **Offscreen** | [`src/offscreen.ts`](../src/offscreen.ts) | DOM-capable background page (Blobs, clipboard, DOM scraping, local storage). | `ExtensionMessage()` + `WindowMessage(window, sandbox)` → `OffscreenManager` |
| **Sandbox** | [`src/sandbox.ts`](../src/sandbox.ts) | `sandbox`ed iframe inside offscreen. Evaluates background/scheduled scripts; runs cron. | `WindowMessage(window, parent)` + `Server("sandbox")` → `SandboxManager` |

Expand Down Expand Up @@ -167,8 +177,9 @@ communication styles** over **several transports**.

| Class | File | Connects | Underlying API |
|---|---|---|---|
| `ExtensionMessage` | [`extension_message.ts`](../packages/message/extension_message.ts) | SW ↔ Content / Inject / Offscreen | `chrome.runtime.sendMessage` / `onConnect` (+ `onUserScript*` on Firefox) |
| `CustomEventMessage` | [`custom_event_message.ts`](../packages/message/custom_event_message.ts) | Content ↔ Inject | DOM `CustomEvent` dispatch (bypasses page tampering) |
| `ExtensionMessage` | [`extension_message.ts`](../packages/message/extension_message.ts) | SW ↔ Content / Inject / Offscreen | `chrome.runtime.sendMessage` / `onConnect`; browser-identified USER_SCRIPT messages are action-gated, and regular-port fallbacks are token-bound |
| `PageMessage` | [`page_message.ts`](../packages/message/page_message.ts) | `scripting` ↔ Inject | `window.postMessage`; page-visible MAIN bootstrap/fallback, runtime updates, whitelisted external API, and GM RPC fallback validated by `PageRpcRegistry` |
| `CustomEventMessage` | [`custom_event_message.ts`](../packages/message/custom_event_message.ts) | Content ↔ `scripting` page helper | DOM `CustomEvent`; bootstrap handoff and synchronous DOM references, not privileged GM RPC |
| `WindowMessage` | [`window_message.ts`](../packages/message/window_message.ts) | Offscreen ↔ Sandbox | `window.postMessage` |
| `ServiceWorkerMessageSend` | [`window_message.ts`](../packages/message/window_message.ts) | SW → Offscreen (Chrome) | `clients.matchAll()` + `postMessage` |
| `MessageQueue` | [`message_queue.ts`](../packages/message/message_queue.ts) | Broadcast among the contexts that instantiate it — SW, Offscreen, UI pages | `chrome.runtime.sendMessage` + local `EventEmitter3` |
Expand Down
31 changes: 30 additions & 1 deletion docs/references/architecture-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,34 @@ The Agent subsystem does not use one persistence pattern; pick by data shape, ma
attachments), `AgentTaskRunRepo` (task run history), `SkillRepo` (skill `.md`/script bundles).
- `MCPServerRepo` (`Repo<T>`) — MCP server configs.

## Userscript resource ownership

The `CAT.agent.*` APIs are granted per script, but a grant alone does not decide which persisted resources that
script can access. The service-worker GM handlers take the caller identity from `request.script.uuid` and pass it
to the Agent services; they do not use a caller-supplied `scriptUuid` as the authority.

- **Conversations** created by a script persist `ownerScriptUuid`. Script reads, chats, attaches, and mutations
check that owner. UI and legacy conversations without an owner remain available to the extension UI but are not
visible to script callers. Ephemeral chats are not persisted conversations.
- **Tasks** created by a script persist `ownerScriptUuid`; script list/get/update/delete/enable/run/history
operations are scoped to that owner. For compatibility, a legacy event task without an owner remains visible
only to the script named by `sourceScriptUuid`.
- **DOM monitors** are scoped to the script UUID supplied by the service-worker GM handler and to the tab. A
script caller cannot peek, stop, or replace a monitor owned by another script.
- **Attachments** live in the shared OPFS workspace and do not carry owner metadata themselves. Before
`CAT.agent.opfs.readAttachment` returns a file, `AgentChatRepo` verifies that a persisted message references
it from a conversation owned by the calling script. A guessed ID or a reference borrowed from another script's
conversation is insufficient.

The checks are implemented in [`gm_agent.ts`](../../src/app/service/service_worker/gm_api/gm_agent.ts),
[`gm_agent_dom.ts`](../../src/app/service/service_worker/gm_api/gm_agent_dom.ts),
[`gm_agent_task.ts`](../../src/app/service/service_worker/gm_api/gm_agent_task.ts),
[`chat_service.ts`](../../src/app/service/agent/service_worker/chat_service.ts),
[`task_service.ts`](../../src/app/service/agent/service_worker/task_service.ts),
[`background_session_manager.ts`](../../src/app/service/agent/service_worker/background_session_manager.ts),
[`opfs_service.ts`](../../src/app/service/agent/service_worker/opfs_service.ts), and
[`dom_cdp.ts`](../../src/app/service/agent/service_worker/dom_cdp.ts).

## Page / offscreen / sandbox delegation and permission boundaries

- **Content (`src/app/service/content/gm_api/cat_agent.ts`)** exposes the `CAT.agent.*` API to user scripts —
Expand All @@ -120,7 +148,8 @@ The Agent subsystem does not use one persistence pattern; pick by data shape, ma
uses CDP; a background (non-active) tab tries CDP first and falls back to `chrome.tabs.captureVisibleTab`
on failure; an active tab with no selector uses `chrome.tabs.captureVisibleTab` directly.
- **Tab monitoring** (`startMonitor`/`stopMonitor`/`peekMonitor`) is unconditionally CDP-based — there is no
non-CDP path for it at all.
non-CDP path for it at all. A monitor is scoped to its tab and initiating script; other scripts cannot
inspect, stop, or replace it.

CDP attaches the debugger to a tab and carries the extra permission/user-visible-banner implications that
come with `chrome.debugger`; how often that applies depends on which action you're looking at, not a single
Expand Down
17 changes: 12 additions & 5 deletions docs/references/architecture-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ go through a controlled context object instead of the page's real globals:
Key points:

- `with(arguments[0]||this.$)` makes every bare identifier resolve against the GM context first. The context is
a `Proxy` that intercepts reads, so the script sees `unsafeWindow`, the granted `GM_*` functions, and a
controlled view of globals — not the raw page scope.
a descriptor-based pseudo-window that projects `unsafeWindow`, the granted `GM_*` functions, and a controlled
view of globals — not the raw page scope. It is a compatibility projection rather than a security membrane.
- Context and script name are passed as **unnamed `arguments`** (`arguments[0]`, `arguments[1]`) so user code
can't shadow them by declaring variables of the same name.
- `.call(this)` preserves `this` because `chrome.userScripts` invokes the function free-standing (an arrow
function would capture the wrong `this`).
- The wrapper installs the body as a temporary method and removes it in the same expression. This preserves the
userscript `this` without resolving mutable page `call`, `apply`, or `bind` properties.

### Path A — Page scripts → `chrome.userScripts`

Expand All @@ -38,7 +38,14 @@ patterns and registers the compiled payload (the `scripting` bundle) with `chrom
`MAIN` or `USER_SCRIPT` world as required. At document time the content/inject pair
([`script_runtime.ts`](../../src/app/service/content/script_runtime.ts),
[`exec_script.ts`](../../src/app/service/content/exec_script.ts)) evaluates the compiled function with the GM
context.
context. The `USER_SCRIPT` content path obtains its matched scripts directly from the service worker over
`ExtensionMessage` after a bootstrap-token handoff. The MAIN `inject` path uses a native extension port for GM RPC
when available; `PageMessage` carries page-visible bootstrap/fallback traffic, MAIN event/value updates, the
whitelisted `external.Scriptcat` API, and the MAIN GM RPC fallback through the `scripting` bundle. That fallback
is checked against the current `PageRpcRegistry` execution handle and grant before it is forwarded to the service
worker. `CustomEventMessage` carries the content bootstrap handoff and synchronous DOM references. Neither
page-visible bridge establishes an authenticated extension origin, so consumers must validate its payloads before
acting on them.

### Path B — Background scripts → Offscreen → Sandbox

Expand Down
11 changes: 8 additions & 3 deletions docs/references/architecture-gm-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ across contexts to a privileged handler, then streams the result back. The imple

- **Content side** ([`src/app/service/content/gm_api/`](../../src/app/service/content/gm_api)) — what runs *near*
the userscript. Synchronous-feeling APIs (`GM_getValue`, `GM_log`) and the client half of async ones
(`GM_xmlhttpRequest`, `GM_setValue`). Built on `GM_Base`, which owns the messaging plumbing.
(`GM_xmlhttpRequest`, `GM_setValue`). Built on `GM_Base`, which owns the request facade. `USER_SCRIPT` calls use
the native extension channel; the DOM helper remains a narrow synchronous `CustomEventMessage` path.
- **Service-worker side** ([`src/app/service/service_worker/gm_api/`](../../src/app/service/service_worker/gm_api))
— the privileged half: permission verification, cross-origin requests, DNR rule building.
- **Offscreen side** ([`src/app/service/offscreen/gm_api.ts`](../../src/app/service/offscreen/gm_api.ts)) —
DOM-dependent operations for background scripts (page-context XHR, `window.open`, clipboard).
- **Values** flow through `ValueService` and are broadcast so every tab running the same script sees updates.
- **Values** flow through `ValueService`. MAIN updates use the scripting broadcast, while USER_SCRIPT updates are
delivered over the native per-document callback port so privileged packets do not cross the page-observable DOM
channel.

### Registration: the `@GMContext.API` decorator

Expand Down Expand Up @@ -82,4 +85,6 @@ traditional GM API: `@GMContext.API` on the content side
[`compat-grant.js`](../../packages/eslint/compat-grant.js). What differs is the naming and transport
shape — the grant is dotted (`CAT.agent.conversation`) and bound with `follow:` rather than `alias:`, the SW
handlers set `dotAlias: false`, and conversation chat streams over `connect()` instead of `sendMessage`. Copy
the nearest existing `CAT.agent.*` method rather than a `GM_*` one.
the nearest existing `CAT.agent.*` method rather than a `GM_*` one. The service-worker handlers derive the script
identity from `request.script.uuid`, then the Agent services enforce persisted resource ownership; see
[`architecture-agent.md`](./architecture-agent.md#userscript-resource-ownership) for the scope and legacy rules.
3 changes: 3 additions & 0 deletions packages/message/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ document),细节见
- service_worker 和 offscreen 之间可以使用 postMessage 的方式进行通信,避免同时监听 message 与 connect 导致冲突的问题。
- service_worker 会在空闲后进入不活动状态;与它建立的 `connect()` 长连接会在此时中断,因此需要长连接的场景要考虑
重连/状态恢复,而不是假定连接一直存活——这不是禁止在 service_worker 上使用 `connect`,只是需要为其生命周期设计容错。
- USER_SCRIPT content 和 MAIN inject 优先使用 `ExtensionMessage` 原生扩展通道;服务端区分浏览器提供的 USER_SCRIPT 来源,并把 MAIN 或专用监听器不可用时的普通端口绑定到文档 bootstrap token。
- `Server("serviceWorker")` 对浏览器标记的 `userScript` 来源仅允许 `connect()` 使用 `runtime/registerUserScript` 或 `runtime/gmApi`,仅允许 `sendMessage()` 使用 `runtime/gmApi` 或 `runtime/reconnectUserScript`;普通 extension 端口不带该来源标记,USER_SCRIPT / MAIN 的注册回退路径会在 `runtime/registerUserScript` 握手中校验文档 bootstrap token。
- `CustomEventMessage` 和 `PageMessage` 是页面可见的桥:前者承载 content bootstrap 交接与同步 DOM 节点引用,后者承载 MAIN bootstrap/fallback、事件/值更新、白名单 `external.Scriptcat` API,以及经 `scripting` 中转的 GM RPC fallback。它们不提供已认证的扩展来源;`PageMessage` 的 GM RPC 在转发前必须通过 `PageRpcRegistry` 的执行句柄与 grant 校验。
11 changes: 8 additions & 3 deletions packages/message/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ export const CustomEventClone = CustomEvent;
const performanceClone = (process.env.VI_TESTING === "true" ? new EventTarget() : performance) as Performance;

// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone);
export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone);
export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone);
const nativeReflectApply = Reflect.apply;
const nativeFunctionBind = Function.prototype.bind;
const bindNative = <T extends (...args: any[]) => any>(fn: T, receiver: any): T =>
nativeReflectApply(nativeFunctionBind, fn, [receiver]) as T;

export const pageDispatchEvent = bindNative(performanceClone.dispatchEvent, performanceClone);
export const pageAddEventListener = bindNative(performanceClone.addEventListener, performanceClone);
export const pageRemoveEventListener = bindNative(performanceClone.removeEventListener, performanceClone);
const detailClone = typeof cloneInto === "function" ? cloneInto : null;
export const pageDispatchCustomEvent = <T = any>(eventType: string, detail: T) => {
if (detailClone && detail) detail = <T>detailClone(detail, performanceClone);
Expand Down
26 changes: 25 additions & 1 deletion packages/message/custom_event_message.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { CustomEventMessage } from "./custom_event_message";
import { createMouseEvent, pageDispatchEvent } from "@Packages/message/common";

Expand All @@ -17,6 +17,30 @@ function createMessagePair() {
}

describe("CustomEventMessage relatedTarget lifecycle", () => {
it("ignores accessor envelopes without executing their getters", () => {
const receiver = new CustomEventMessage(`custom-event-message-test-${++flagCounter}`, true, "");
const received = vi.fn();
receiver.onMessage(received);
const envelope: Record<string, unknown> = {
messageId: "hostile",
type: "sendMessage",
data: { action: "custom-event-message-test/hostile" },
};
let accessed = false;
Object.defineProperty(envelope, "data", {
configurable: true,
enumerable: true,
get() {
accessed = true;
throw new Error("page getter executed");
},
});

expect(() => receiver.messageHandle(envelope as any, { postMessage: vi.fn() })).not.toThrow();
expect(accessed).toBe(false);
expect(received).not.toHaveBeenCalled();
});

it("stores a received target on the receiving message until it is consumed", () => {
const { sender, receiver } = createMessagePair();
const target = document.createElement("div");
Expand Down
Loading
Loading