@longzai-intelligence-oxlint/architecture-electron-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @longzai-intelligence-oxlint/architecture-electron-plugin
2
+
3
+ ## 意图
4
+
5
+ 在静态检查阶段拦截 Electron 安全反模式:平台 API 泄漏到渲染进程、Renderer 内放入实现代码、Preload API 过度暴露、IPC 通道未类型化、IPC 结果未解包等。
6
+
7
+ ## 定位
8
+
9
+ `architecture` 插件族的 Electron 子插件,承载 Electron 进程隔离与 IPC 安全约束类规则。从原 `plugin-architecture` 单包拆分而来(ADR 0002)。
10
+
11
+ ## 规则
12
+
13
+ - `platform-leak` — 平台 API 泄漏检测
14
+ - `no-implementation-in-renderer` — Renderer 内禁止实现代码
15
+ - `no-unsafe-electron-api-exposure` — 不安全 Electron API 暴露
16
+ - `no-generic-ipc-bridge-exposure` — 通用 IPC bridge 暴露
17
+ - `no-direct-ipcrenderer-in-preload` — Preload 直接使用 ipcRenderer
18
+ - `require-typed-ipc-channel` — IPC 通道类型化
19
+ - `require-expose-safe-api` — 暴露安全 API
20
+ - `no-custom-ipc-utility` — 自定义 IPC 工具
21
+ - `require-unwrap-ipc-result` — IPC 结果解包
@@ -0,0 +1,21 @@
1
+ //#region src/rules/index.d.ts
2
+ /**
3
+ * electron 规则集合
4
+ */
5
+ declare const electronRules: {
6
+ 'platform-leak': import("@oxlint/plugins").Rule;
7
+ 'no-implementation-in-renderer': import("@oxlint/plugins").Rule;
8
+ 'no-unsafe-electron-api-exposure': import("@oxlint/plugins").Rule;
9
+ 'no-generic-ipc-bridge-exposure': import("@oxlint/plugins").Rule;
10
+ 'no-direct-ipcrenderer-in-preload': import("@oxlint/plugins").Rule;
11
+ 'require-typed-ipc-channel': import("@oxlint/plugins").Rule;
12
+ 'require-expose-safe-api': import("@oxlint/plugins").Rule;
13
+ 'no-custom-ipc-utility': import("@oxlint/plugins").Rule;
14
+ 'require-unwrap-ipc-result': import("@oxlint/plugins").Rule;
15
+ };
16
+ /**
17
+ * architecture-electron 子插件
18
+ */
19
+ declare const architectureElectronPlugin: import("@oxlint/plugins").Plugin;
20
+ //#endregion
21
+ export { architectureElectronPlugin, architectureElectronPlugin as default, electronRules };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{defineRule as e,eslintCompatPlugin as t}from"@oxlint/plugins";const n=`createTyped`;function r(e,t){let n=e.replace(/\\/g,`/`);return n.includes(t)&&!n.includes(`.test.`)}function i(e){let t=Array.isArray(e.options)?e.options[0]:void 0,r=t&&typeof t==`object`?t:{};return{preloadUtilsPath:`preloadUtilsPath`in r&&typeof r.preloadUtilsPath==`string`?r.preloadUtilsPath:`/src/preload/utils/`,ipcUtilityPrefix:`ipcUtilityPrefix`in r&&typeof r.ipcUtilityPrefix==`string`?r.ipcUtilityPrefix:n}}const a=e({meta:{type:`problem`,docs:{description:`禁止自定义 IPC 工具函数,应从标准库重新导出`}},createOnce(e){let t=!1,a=n;return{before(){let{preloadUtilsPath:n,ipcUtilityPrefix:o}=i(e);if(a=o,t=r(e.filename,n),!t)return!1},FunctionDeclaration(n){t&&n.id&&n.id.name.startsWith(a)&&e.report({node:n,message:`禁止自定义 IPC 工具函数 "${n.id.name}"。应从标准库重新导出。`})},VariableDeclaration(n){if(t)for(let t of n.declarations)t.id.type===`Identifier`&&t.id.name.startsWith(a)&&(t.init?.type===`FunctionExpression`||t.init?.type===`ArrowFunctionExpression`)&&e.report({node:t,message:`禁止自定义 IPC 工具函数 "${t.id.name}"。应从标准库重新导出。`})}}}});function o(e){let t=e.replace(/\\/g,`/`);return t.includes(`/src/preload/`)&&!t.includes(`/src/preload/utils/`)&&!t.includes(`.test.`)}const s=e({meta:{type:`problem`,docs:{description:`禁止在 Preload 中直接调用 ipcRenderer`}},createOnce(e){return{before(){if(!o(e.filename))return!1},CallExpression(t){t.callee.type===`MemberExpression`&&t.callee.object.type===`Identifier`&&t.callee.object.name===`ipcRenderer`&&t.callee.property.type===`Identifier`&&e.report({node:t,message:`禁止直接调用 ipcRenderer.${t.callee.property.name}()。请使用 createTypedAPI 或 invoke 封装。`})}}}}),c=new Set([`on`,`send`,`invoke`]);function l(e){let t=e.replace(/\\/g,`/`);return t.includes(`/src/preload/`)&&!t.includes(`.test.`)}function u(e){for(let t of e)if(typeof t==`object`&&t&&`key`in t&&t.key!==null&&typeof t.key==`object`&&`name`in t.key&&typeof t.key.name==`string`&&c.has(t.key.name))return!0;return!1}const d=e({meta:{type:`problem`,docs:{description:`禁止暴露包含通用 IPC 方法的桥接对象`}},createOnce(e){return{before(){if(!l(e.filename))return!1},Property(t){t.key.type!==`Identifier`||t.key.name!==`ipc`||t.value.type===`ObjectExpression`&&u(t.value.properties)&&e.report({node:t,message:`禁止暴露包含 on/send/invoke 方法的 ipc 对象。请使用领域 API 替代通用 IPC 方法。`})}}}}),f=[`/src/renderer/`];function p(e,t){let n=e.replace(/\\/g,`/`);return t.some(e=>n.includes(e))}function m(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e,t,n){return m(e[t])?e[t]:n}function _(e,t,n){return typeof e[t]==`string`?e[t]:n}const v={},y=e({meta:{type:`problem`,docs:{description:`禁止在 Renderer 进程导入 Implementation 包`}},createOnce(e){let t=f,n=``,r=`-implementation`;return{before(){let i=e.options?.[0],a=h(i)?i:v;if(t=g(a,`rendererPatterns`,f),n=_(a,`packageScope`,``),r=_(a,`implementationSuffix`,`-implementation`),!p(e.filename,t))return!1},ImportDeclaration(t){let i=t.source.type===`Literal`&&typeof t.source.value==`string`?t.source.value:null;i&&(n&&!i.startsWith(n)||i.endsWith(r)&&e.report({node:t,message:`Renderer 进程禁止导入 Implementation 包 "${i}"。请使用 Contract 包替代。`}))}}}});function b(e,t){if(e.callee.type!==`MemberExpression`||e.callee.object.type!==`Identifier`||e.callee.object.name!==`contextBridge`||e.callee.property.type!==`Identifier`||e.callee.property.name!==`exposeInMainWorld`||e.arguments.length<2)return;let n=e.arguments[0];if(n?.type===`Literal`&&n.value===`electron`){let n=e.arguments[1];n?.type===`Identifier`&&n.name===`electronAPI`&&t(e,`禁止使用 contextBridge.exposeInMainWorld("electron", electronAPI) 暴露全部 Electron API。请使用领域 API 替代。`)}}function x(e,t){e.left.type===`MemberExpression`&&(e.left.object.type!==`Identifier`||e.left.object.name!==`window`||e.left.property.type!==`Identifier`||e.left.property.name!==`electron`||e.right.type===`Identifier`&&e.right.name===`electronAPI`&&t(e,`禁止通过 window.electron = electronAPI 暴露全部 Electron API。请使用领域 API 替代。`))}const S=e({meta:{type:`problem`,docs:{description:`禁止直接暴露 electronAPI 给渲染进程`}},createOnce(e){return{ImportDeclaration(t){if(!(t.source.type!==`Literal`||t.source.value!==`@electron-toolkit/preload`))for(let n of t.specifiers)n.type===`ImportSpecifier`&&n.imported.type===`Identifier`&&n.imported.name===`electronAPI`&&e.report({node:n,message:`禁止导入 electronAPI 并直接暴露给渲染进程。请使用领域 API 替代。`})},CallExpression(t){b(t,(t,n)=>{e.report({node:t,message:n})})},AssignmentExpression(t){x(t,(t,n)=>{e.report({node:t,message:n})})}}}}),C=new Set(`fs.path.os.crypto.stream.http.https.net.tls.child_process.cluster.dgram.dns.domain.events.punycode.querystring.readline.repl.string_decoder.timers.tty.url.util.v8.vm.worker_threads.zlib.fs/promises.stream/promises.stream/consumers.stream/web.node:fs.node:path.node:os.node:crypto.node:stream.node:http.node:https.node:net.node:tls.node:child_process.node:cluster.node:dgram.node:dns.node:events.node:punycode.node:querystring.node:readline.node:repl.node:string_decoder.node:timers.node:tty.node:url.node:util.node:v8.node:vm.node:worker_threads.node:zlib.node:fs/promises`.split(`.`)),w=new Set([`window`,`document`,`navigator`,`localStorage`,`sessionStorage`,`fetch`,`XMLHttpRequest`,`WebSocket`,`Worker`,`Blob`,`File`,`FileReader`,`URL`,`URLSearchParams`,`FormData`,`Headers`,`Request`,`Response`,`History`,`Location`,`Screen`]),T=new Set([`ipcRenderer`,`ipcMain`,`BrowserWindow`,`app`,`Menu`,`MenuItem`,`dialog`,`shell`,`clipboard`,`nativeImage`,`Tray`,`webContents`,`session`,`protocol`,`net`,`powerMonitor`,`powerSaveBlocker`,`screen`,`systemPreferences`,`TouchBar`,`crashReporter`]),E=[`/domains/shared-kernel/`];function D(e,t){let n=e.replace(/\\/g,`/`);return t.some(e=>n.includes(e))}function O(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function k(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function A(e){if(O(e)&&`domainLayerPatterns`in e){let t=e.domainLayerPatterns;if(k(t))return t}return E}function j(e,t){let n=e.source.type===`Literal`&&typeof e.source.value==`string`?e.source.value:null;n&&(C.has(n)||n.startsWith(`node:`))&&t(e,`域层禁止导入 Node.js 核心模块 "${n}"。域层应保持平台无关性。`)}function M(e,t){if(e.object.type!==`Identifier`)return;let n=e.object.name;w.has(n)&&t(e,`域层禁止使用浏览器 API "${n}"。域层应保持平台无关性。`),T.has(n)&&t(e,`域层禁止使用 Electron API "${n}"。域层应保持平台无关性。`)}function N(e,t){(w.has(e.name)||T.has(e.name))&&(e.parent?.type!==`MemberExpression`||e.parent.object!==e)&&t(e,`域层禁止直接引用平台 API "${e.name}"。域层应保持平台无关性。`)}const P=e({meta:{type:`problem`,docs:{description:`禁止在业务域层使用平台特定 API(Node.js / Browser / Electron)`}},createOnce(e){let t=null;return{before(){if(t===null){let n=e.options?.[0];t=A(n)}let n=e.filename;if(!D(n,t))return!1},ImportDeclaration(t){j(t,(t,n)=>{e.report({node:t,message:n})})},MemberExpression(t){M(t,(t,n)=>{e.report({node:t,message:n})})},Identifier(t){N(t,(t,n)=>{e.report({node:t,message:n})})}}}});function F(e){let t=e.replace(/\\/g,`/`);return t.endsWith(`/src/preload/index.ts`)&&!t.includes(`.test.`)}const I=e({meta:{type:`problem`,docs:{description:`强制使用 exposeSafeAPI 暴露 API`}},createOnce(e){return{before(){if(!F(e.filename))return!1},CallExpression(t){t.callee.type===`MemberExpression`&&(t.callee.object.type!==`Identifier`||t.callee.object.name!==`contextBridge`||t.callee.property.type!==`Identifier`||t.callee.property.name!==`exposeInMainWorld`||e.report({node:t,message:`禁止直接调用 contextBridge.exposeInMainWorld。请使用 exposeSafeAPI 进行 API 暴露。`}))}}}});function L(e){let t=e.replace(/\\/g,`/`);return t.includes(`/src/preload/`)&&!t.includes(`.test.`)}const R=e({meta:{type:`problem`,docs:{description:`禁止使用字符串字面量作为 IPC 通道名`}},createOnce(e){return{before(){if(!L(e.filename))return!1},CallExpression(t){if(t.callee.type!==`Identifier`||t.callee.name!==`invoke`||t.arguments.length===0)return;let n=t.arguments[0];n?.type===`Literal`&&typeof n.value==`string`&&e.report({node:n,message:`禁止使用字符串字面量 "${n.value}" 作为 IPC 通道名。请使用常量引用。`})},ObjectExpression(t){if(t.parent?.type===`CallExpression`&&!(t.parent.callee.type!==`Identifier`||t.parent.callee.name!==`createTypedAPI`))for(let n of t.properties)n.type===`Property`&&n.value.type===`Literal`&&typeof n.value.value==`string`&&e.report({node:n.value,message:`禁止在 createTypedAPI 配置中使用字符串字面量 "${n.value.value}" 作为 IPC 通道名。请使用常量引用。`})}}}});function z(e){let t=e.replace(/\\/g,`/`);return t.includes(`/src/renderer/`)&&!t.includes(`.test.`)}function B(e){return e.callee.type!==`MemberExpression`||e.callee.object.type!==`MemberExpression`?!1:e.callee.object.object.type===`Identifier`&&e.callee.object.object.name===`window`}function V(e){return typeof e==`object`&&!!e&&`type`in e&&`callee`in e}function H(e){return typeof e==`object`&&!!e&&`parent`in e}function U(e){let t=e;for(;t&&H(t);){let e=t.parent;if(V(e)&&e.callee){if(e.callee.type===`Identifier`&&(e.callee.name===`unwrap`||e.callee.name===`unwrapIPCResult`))return!0;if(e.callee.type===`Identifier`)return!1}t=e}return!1}const W={"platform-leak":P,"no-implementation-in-renderer":y,"no-unsafe-electron-api-exposure":S,"no-generic-ipc-bridge-exposure":d,"no-direct-ipcrenderer-in-preload":s,"require-typed-ipc-channel":R,"require-expose-safe-api":I,"no-custom-ipc-utility":a,"require-unwrap-ipc-result":e({meta:{type:`problem`,docs:{description:`渲染层调用 IPC API 时必须使用 unwrap 解包结果`}},createOnce(e){return{before(){if(!z(e.filename))return!1},CallExpression(t){B(t)&&(U(t)||e.report({node:t,message:`渲染层调用 IPC API 时必须使用 unwrap 或 unwrapIPCResult 解包结果。请使用 unwrap(window.xxx.method())。`}))}}}})},G=t({meta:{name:`@longzai-intelligence-oxlint/architecture-electron-plugin`},rules:W});export{G as architectureElectronPlugin,G as default,W as electronRules};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@longzai-intelligence-oxlint/architecture-electron-plugin",
3
+ "version": "0.1.0",
4
+ "license": "UNLICENSED",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "lzi-builder",
19
+ "build:prod": "NODE_ENV=production lzi-builder",
20
+ "prepublishOnly": "bun run build:prod",
21
+ "dev": "lzi-builder --watch",
22
+ "clean": "rimraf dist",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ "lint": "oxlint && oxfmt --check",
27
+ "lint:fix": "oxlint --fix && oxfmt",
28
+ "typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
29
+ "typecheck:app": "tsgo --noEmit -p tsconfig/app.json",
30
+ "typecheck:node": "tsgo --noEmit -p tsconfig/node.json",
31
+ "typecheck:test": "tsgo --noEmit -p tsconfig/test.json"
32
+ },
33
+ "dependencies": {
34
+ "@longzai-intelligence-oxlint/core": "0.1.0",
35
+ "@oxlint/plugins": "^1.74.0"
36
+ },
37
+ "devDependencies": {
38
+ "@longzai-intelligence-oxlint/testing": "0.1.0",
39
+ "@vitest/coverage-v8": "^4.1.10",
40
+ "typescript": "^6.0.3"
41
+ },
42
+ "peerDependencies": {
43
+ "oxlint": ">=1.0.0"
44
+ }
45
+ }