把第三方代码装进生产实例:TREK 插件管理要过的 5 道关
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
TREK 的插件系统允许第三方在不改一行 TREK 源码的前提下,往自托管实例里装新能力——仪表盘 widget、独立页面、行程页标签、照片/日历/通知渠道集成。自托管管理员的 TREK 插件管理集中在Admin → Plugins面板,而把第三方代码引入生产实例,实际要过五道关:安装关(SHA-256 + 作者签名 + manifest 复校验 + 版本兼容)、审查关(预安装对话框逐条展示权限与出口主机)、启用关(显式开关 + 权限同意,拒绝原因带结构化错误码)、更新关(扩权必须重新同意、换钥必须重信任)、审计关(哈希链能力审计,用户本人可见)。本文按运维流程走完这五道关,并用平实语言解释每个操作背后为什么这样设计。读完本文,你将能在生产实例上安全地跑完一个插件的完整生命周期,并清楚每个徽章、每条权限、每个错误码保证什么、不保证什么。
开工前:谁能进、哪些开关要开
核心事实:整个面板只接受管理员账户,且每个端点还叠加一层插件运行时总开关;总开关默认开启,但"运行时开着"不等于"任何代码会跑"——每个已安装插件都必须逐个手动激活。
服务端所有路由挂在api/admin/plugins之下,同时套JwtAuthGuard与AdminGuard(plugins.controller.ts)。普通用户连已安装列表都列不出来。总开关由TREK_PLUGINS_ENABLED控制,判定逻辑在 kill-switch.ts:取值只要不是false、0、off、no(大小写不敏感)就算开启,且每次调用实时读取——改环境变量重启后立即生效。关闭时install、upload、activate、update、rescan等操作统一返回 503,面板会显示关闭横幅;已安装插件保留在磁盘上(停用状态、无害),重新打开即可恢复。
environment: - TREK_PLUGINS_ENABLED=false与插件管理相关的其余环境变量:
| 环境变量 | 作用 | 默认值 |
|---|---|---|
TREK_PLUGINS_ENABLED | 插件系统总开关;关闭时对应操作一律 503 | 开启 |
TREK_PLUGINS_DEV_LINK | 开发专用:允许从本地构建目录注册插件并热重载,仅当值恰为1时生效(dev-link.ts) | 关闭 |
TREK_PLUGIN_ALLOW_PRIVATE_EGRESS | 设为on时允许插件出口访问私网/局域网地址(localhost、192.168.x.x等)。⚠️ 这会放宽所有已安装插件的私网出口策略,只有你信任全部插件时才应开启 | 关闭(默认拒绝私网出口) |
TREK_PLUGIN_REGISTRY_URL | 覆盖 Discover 视图浏览的注册表索引地址,可指向自己的 fork/镜像 | TREK 官方注册表(聚合的dist/index.json) |
TREK_PLUGIN_MAX_RSS_MB | 单个插件子进程的硬内存上限(RSS,不是 V8 堆) | 300 MB(plugin-supervisor.ts) |
第一次安装:标准路径
核心事实:注册表安装 = SSRF 安全的下载 → SHA-256 校验 → 防 zip-slip/zip-bomb 解压 → manifest 严格复校验 → 原生二进制重扫 → 原子落盘 → 注册为未激活。全程不执行任何插件代码,执行是之后独立的一步。
流程分三步:切到Discover视图 → 点卡片打开审查对话框 → 点Install。
Discover 卡片上的每个字段
每张卡片展示:图标、名称、作者、描述、类型(Widget / Page / Integration / Trip page)、Reviewed徽章(如适用)、Signed/Unsigned徽章、最新版本号、累计下载量。两个徽章的含义在「你可以信什么」一节逐一说破,安装前不必先读懂它们,但要知道它们不说明代码做了什么。
安装前必看的审查对话框
点卡片后打开的对话框才是决策依据,四个必看项:
- What it can access(可访问范围)——插件请求的权限逐条用平实语言渲染(如读取行程、创建与编辑地点)。未知权限代码原样显示;一个权限都不请求时,明确标注无需特殊访问。
- Connects to(连接目标)——manifest 声明的全部出口主机,等宽字体 chip。
- Setup(配置项)——插件将要求填写的设置,标注Instance-wide(实例级)还是Per user(用户级)、是否Required。
- Details(详情)——版本、体积、所需 TREK 版本范围、审查时间、下载量,附Source repository、Report an issue、Homepage链接。
关键机制:这些信息不是作者自己写的介绍文字。服务端会拉取插件在审查提交点的实时 manifest 生成预览(ManifestPreview,registry.service.ts),包含权限列表、出口主机、operatorEgress标记、设置字段(key/label/inputType/scope/required)、许可证、所需 addon 与插件依赖。拉取失败时软降级,对话框仍用注册表元数据渲染。
版本兼容:Install 能装什么
兼容性由服务端计算,客户端刻意不持有第二套 semver 实现——UI 若与安装闸门意见不一致,会出现"按钮亮着却 400"的假象(registry.service.ts)。三种结果:
- 最新版兼容 → 正常Install。
- 最新版要求更新的 TREK,但存在兼容旧版 → 按钮变为Install {version}(装那个旧版),对话框在琥珀色提示条里解释原因,不藏在 tooltip 里。
- 没有任何版本兼容 → 按钮变为Incompatible并禁用。
还有一层更硬的校验:下载后、解压后,安装管线用归档自带的 manifest再查一次版本范围(registry.service.ts)。注册表索引里的 min/max 只是较弱的预下载过滤(比如表达不了<4.0.0这种排他上限),真正的判定以工件自身声明为准。
"装完默认关闭"的确切语义
安装成功时,插件状态是已安装但未激活:代码在磁盘、注册表有行、权限已登记,但子进程不存在,不运行任何代码。安装流程做了:下载、完整性与签名校验、安全解压、manifest 复验、原生二进制拒绝、原子替换、登记 inactive;没做:任何权限授予、任何进程派生。
插件要连"只有你知道地址"的服务(operatorEgress)
自托管 Gotify、ntfy 这类服务的主机名在插件发布时不可能写死。声明了operatorEgress的插件,审查对话框会额外出现一个「+ hosts you add」chip,提示安装后由管理员补填主机。安装后在⋯ → Allowed hosts逐个添加;在添加至少一个主机前,该行显示琥珀色Add allowed hostchip——因为此时它一个主机都到不了,不提示会像静默故障。
保存后插件会被重启以加载新白名单:出口守卫在子进程初始化时安装一次且拒绝二次init,运行中插件的白名单永远不能在原进程内热扩容(plugin-runtime.service.ts)。主机校验与 manifest 声明出口同一套规则:不允许裸*、不允许整 TLD 通配、不允许带协议前缀(校验正则见 plugin-runtime.service.ts)。三条硬边界:未声明operatorEgress的插件永远无法被授予主机(安装时同意的范围仍是硬边界);只有管理员能添加主机,普通用户永远不能扩大出口;服务若在 TREK 同机或同局域网,还需TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on。删除某个主机后插件立即失去该出口并再次重启;卸载时这些主机一并删除。
两条旁路:不走官方渠道装
核心事实:两条旁路都以未激活状态注册,激活时同样要走权限同意;来源徽章(Sideloaded/Dev-Link)是卡片上的诚实标签——它记录代码来源,但不会因此多做一道检查、换一种沙箱或加一层限制。
上传 .zip:旁路加载
用工具栏的Upload plugin按钮,或直接把.zip拖到面板上。服务端sideload()分两步:先解压到 staging 并执行与注册表安装相同的硬防护(防 slip/炸弹的安全解压、严格 manifest 校验、拒绝原生二进制),仅注册表的 SHA-256/签名校验不适用(因为没有注册表条目可依据,registry.service.ts);通过后再原子替换代码目录(plugin-runtime.service.ts)。
限制与语义:
- 上传上限 50 MB(与 SDK
pack同上限,另留 4 KB 归档开销,plugins.controller.ts)。 - 该行标记Sideloaded:Uploaded manually — not from the registry, unsigned and unreviewed.——手动上传,未签名、未经注册表审查。
- 用相同 id 覆盖上传时,旧代码先被强制停止并停用再替换,且新代码一律登记为 inactive、清除签名密钥与任何更新阻塞记录(registry.service.ts)——替换后的代码绝不处于"未经重新激活仍在运行"的状态。
链接本地插件:dev-link
面板出现Link a local plugin路径输入框,当且仅当服务端设了TREK_PLUGINS_DEV_LINK=1(叠加管理员 + 总开关两道门,plugins.controller.ts)。它做的事:对插件代码目录创建符号链接(不复制)、校验 manifest、拒绝原生二进制、注册为 inactive,然后对构建输出目录起fs.watch,重建后防抖 400 ms 自动重新 fork(plugin-runtime.service.ts)。
为什么仅限开发环境(dev-link.ts 注释):dev-linked 插件的构建产物会在重启之间变化且不走重新同意,绕开了安装时的签名/完整性模型;且在npm run dev下 OS 权限 jail 是关的。它的数据访问仍走同一套能力 RPC(成员校验、无冒充),但正因为前两条,加载未签名本地代码必须钉在一个显式开关后面——生产实例上这个入口不应出现。另外,已正式安装(注册表/旁路)的插件不会被同 id 的 dev-link 覆盖,须先卸载(plugin-runtime.service.ts)。
让它跑起来,并养好它
核心事实:启用是面板行上的Enable plugin开关,翻上去 = 授予已审查的权限 + 派生隔离子进程,翻下来 = 立即停进程、保留数据。每个状态词是精确的:Active(子进程在跑)、Off(已安装未激活)、Error(启动/运行失败)、disabled(运行时总开关关闭)。
启用被拒怎么办:原因 → 补救清单
预检按"最严重到最不严重"排序且全程只读,任何一项不满足都不会留下半激活状态(plugin-runtime.service.ts)。每种拒绝都带结构化错误码和对应补救路径:
ADDON_DISABLED——必需的 addon 未启用 → 在Admin → Addons打开对应 addon,重试。DEPENDENCY_MISSING——依赖的插件缺失或版本不在声明范围 → 依赖对话框逐条列出,每个条目带Download / Update一键拉取最新兼容版本后自动重试。TREK_VERSION_INCOMPATIBLE——插件声明的 TREK 范围不含当前宿主版本 → 升级 TREK;这也是升级后"装好的插件突然启用不了"的常见原因。TREK_VERSION_UNKNOWN——插件未声明支持的 TREK 范围,无法判断兼容 → 联系作者补声明。CONSENT_REQUIRED——更新拓宽了权限(见下节)→ 同意对话框里选Approve & turn on或Keep off for now。DEPENDENCY_CYCLE——插件依赖成环 → 修改 manifest 依赖声明后重装。
图标块上的健康圆点同步反映运行时状态:绿(active)、蓝闪(starting)、红(error)、琥珀(disabled/incompatible)、淡色(inactive)。每行的⋯菜单提供Restart(仅活动插件)、View error log、Allowed hosts、Source repository与Report an issue(仅注册表插件)、Delete。
级联语义:依赖与 addon 如何连坐
- 启用一个插件前,先按依赖图算出启动顺序,先拉起所有依赖再拉起目标;已安装但停用的依赖会被自动级联启用,toast 告知哪些被顺带打开(plugin-runtime.service.ts)。
- 某个必需 addon 被关闭时,依赖它的插件自动停用,且所有传递依赖它的插件一并停用(plugin-runtime.service.ts)。
- 停用一个被其他插件依赖的插件,会连带停用所有依赖它的插件——插件不能在依赖缺失下继续运行(plugin-runtime.service.ts,被依赖方先停、依赖方后停)。
更新:权限与出口永不静默扩大
有新版本时,行上出现Update → v{version},列表上方出现更新计数提示条与Update all批量按钮。服务端update()先把新版本声明的权限与已授予权限做差集(plugin-runtime.service.ts):
- 无新增(权限与出口主机都没有)→ 透明重启到新代码;
- 有任何新增 → 新代码安装后插件保持关闭,差集返回给界面:
{name} v{version} is asking for rights you haven't granted yet. The new version is installed but stays off until you approve it.
即:新版装好了但不开,管理员在对话框里看到Newly requested permissions与New outbound connections后,选Approve & turn on或Keep off for now。批量更新时这些同意提示排队依次出现,一个都不跳过。
更新目标的选择同样防坑:resolveUpdateTarget取的是当前 TREK 能运行的最新兼容版本而非字面最新版;若"兼容"版本反而比已装版本旧,直接以NO_COMPATIBLE_UPDATE拒绝——更新语义下静默降级比不更新更糟(plugin-runtime.service.ts)。
更新被签名密钥变更挡住怎么办
作者的签名密钥与安装时钉住的密钥不再匹配时,更新被拒,行上显示Update blocked — {reason}与Review链接,对话框并排展示钉住的密钥指纹与当前提供的密钥指纹:
TREK cannot tell a legitimate key rotation apart from a takeover — both look identical from here. Confirm the new key with the author through a channel you already trust before you accept it.
即:TREK 无法从这一侧区分良性换钥与接管,接手前必须通过你已信任的渠道向作者核实新密钥。
覆盖范围被严格限定:只有SIGNATURE_KEY_CHANGED可以覆盖(Trust the new key & update)。SIGNATURE_INVALID(签名无效)、SIGNATURE_MISSING(先签名后变未签名)、SIGNATURE_INCOMPLETE(半声明:只有密钥没有签名或反之)只给解释,没有任何覆盖按钮,服务端/retrust端点同样拒绝这三种情形(registry.service.ts 的assertRetrustable在服务端重新推导条件,UI 藏按钮只是便利,不是控制)。重信任要求回显对话框里展示的完整公钥——注册表条目若在渲染后被再次换钥,请求被拒;且重信任与更新在同一次调用内完成:要么新密钥通过校验、插件落到新版本并钉住新密钥,要么什么都不变,杜绝"钉住一个从未验证过的密钥"的窗口。该操作同时写入管理员审计日志(含新旧密钥指纹,plugin-runtime.service.ts)。
卸载:删什么、什么无条件删、什么义务延续
⋯ → Delete的确认框会说明:停止插件、移除代码、删除其全部数据(可选保留),不可撤销。服务端uninstall(plugin-runtime.service.ts)分三档:
- 总是删:停止子进程、移除代码目录、
plugins注册表行、设置字段;出口主机与定时任务无条件删除——否则后续复用同一 id 的插件会悄悄继承前人的出口白名单和回调;若是通知渠道插件,渠道一并退役(避免继承所有用户的选择退出)。 - 勾选删除数据时额外删:插件自有数据目录、错误日志、每用户配置(含加密密钥)、OAuth 令牌与状态、迁移台账、能力审计日志、GDPR 擦除队列。
- 保留数据时延续的义务:待处理的用户数据擦除队列行被保留——数据目录还在(可能仍含已注销用户的行),擦除义务必须跟着走;同 id 插件重装后会继续兑现(plugin-runtime.service.ts)。dev-linked 插件卸载只移除符号链接,绝不触碰作者源码目录。
你可以信什么:徽章隔离与它们的边界
核心事实:徽章与隔离机制是两回事——徽章是注册表层面的信任标记,隔离是进程层面的物理边界。前者可被绕过(未签名插件照样装),后者不能。
每个徽章:保证什么,不保证什么
- Reviewed——"Reviewed" means a TREK maintainer scanned this plugin for malware on each version — not for quality or whether it works. It is not a guarantee that a plugin is harmless.即:每个版本都做过恶意软件扫描;不保证质量、可用性,也不是"无害"担保。
- Signed——安装时文件字节已对照作者签名密钥校验,且该密钥在首次安装时被 TOFU 钉住。checksum 证明文件是注册表担保的字节,签名进一步证明这些字节来自作者;不保证代码行为符合作者描述,也不能阻止作者故意写恶意逻辑。
- Unsigned——文件匹配注册表担保的字节,但没有东西把它绑定到作者。少一层保证,不是不安全;目前注册表里多数插件未签名,所以这是琥珀色提示而非警报。
- Sideloaded / Dev-Link——对自己诚实:这些只是卡片上的标签。它们记录代码来源,不触发额外检查、不同沙箱、不同限制;旁路加载的插件以它声明的权限原样运行,与注册表插件一样,且没有恶意软件扫描、没有签名。徽章的存在只为让你一眼看出:除了你,没有人为此代码背书。
隔离模型:插件物理上做不到什么
每个活动插件运行在独立 OS 子进程中,由 Node 权限模型(--permission)启动,文件系统读取限定在其自身代码目录(plugin-supervisor.ts 与 wiki/Plugins.md):
- 无法访问
JWT_SECRET、数据库连接或任何 TREK 机密——这些对其进程物理不可达。 - 不能打开
trek.db、写文件、派生子进程、使用 worker 线程、加载原生模块;其自有数据是独立 SQLite 文件,且只能通过 TREK 访问。 - 只通过内部 RPC 通道与 TREK 通信,TREK 只应答 manifest声明且你批准的能力;未授予的调用被拒绝而非忽略。
- RPC 通道本身对插件代码是封死的:插件的原始 IPC 原语(
process.send、process.on('message'))在其代码加载前被吊销——它既不能伪造宿主消息(伪造路由表、冒充其他请求身份),也不能窃听其他在途请求,一切交互被迫经过能力校验的 SDK。 - 页面/widget 界面运行在密封的浏览器 frame中,读不到会话 cookie,碰不到外围 TREK 页面。
- 崩溃、挂起、内存耗尽时只有它自己的进程死掉:supervisor 带退避重启(5 分钟窗口内最多 5 次崩溃、退避上限 30 秒,超限自动停用),TREK 继续运行。
最坏情况:权限清单是真实边界,不是标签
⚠️ 你批准的权限列表是真实边界,但它界定的是插件能触及什么,不约束它在授权范围内的意图:一个被允许读取行程且能连接某主机的插件,完全可以把行程数据发往那里。它能做到这一步;它做不到的是读未授权的数据、碰 TREK 机密、绕过成员校验。所以安装前读权限清单和出口主机,不是走形式,而是唯一一道由你控制的关口。
审计:谁做了什么、谁看得见
核心事实:TREK 为插件维护一条哈希链防篡改的能力审计,每个宿主中介动作在"插件够不到的地方"落记录——操作者由宿主绑定(插件无法自称以谁的名义)、触达的资源、结果(wiki/Audit-Log.md)。
两个视图,面向两类人:
- 管理员:在Admin → Plugins看到按插件维度的视图(
GET /api/admin/plugins/:id/audit)——某插件对所有用户做过的每次核心数据读取、广播、通知、AI 调用与跨插件调用。 - 每个最终用户:在Settings → Plugins看到以他本人名义发生的全部插件动作,跨插件、最新在前(
GET /api/plugin-activity)。该视图不设管理员门槛——这是刻意设计,让宽泛的只读授权对数据所属者本人可问责。
关键权限的确切授予范围(完整清单见 wiki/Plugin-Permissions.md):
- 只读类
db:read:trips——经ctx.trips读行程/地点/预订/日程等;每个调用都对操作者做 membership 校验,插件读不到该用户看不到的行程。 - 写入类
db:write:places——创建/更新/删除地点;要求行程访问+place_edit权限,输入按 TREK schema 校验,每次写入都审计。 - 宿主中介类
notify:send——收件人被强制限定为操作者本人或其所属行程,拒绝scope:'admin',插件只提供纯文本标题/正文(200/1000 字符上限)与相对路径链接。 - 宿主中介类
oauth:client——宿主跑完整个 OAuth 流程(PKCE + state)并持有client secret 与 refresh token,令牌按用户加密存储;插件只拿到操作者名下短期access token。
每插件的审计保留行数由TREK_PLUGIN_AUDIT_MAX_ROWS封顶(默认 20000 行),保留窗口内哈希链保持防篡改性质(wiki/Environment-Variables.md)。
决策速查
| 状态 / 错误提示 / 错误码 | 含义 | 处理方式 |
|---|---|---|
| 面板显示 "turned off (TREK_PLUGINS_ENABLED)" | 运行时总开关关闭,操作一律 503 | 打开TREK_PLUGINS_ENABLED并重启;已装插件仍在磁盘 |
| 按钮Incompatible(禁用) | 没有任何版本兼容当前 TREK | 升级 TREK,或等作者发兼容版本 |
| 按钮变为Install {version} | 最新版要求更新的 TREK,旧版仍兼容 | 安装提示的旧版本,或升级 TREK 后更新 |
CONSENT_REQUIRED(409) | 新版本拓宽了权限/出口,需重新同意 | 对话框选Approve & turn on或Keep off for now |
ADDON_DISABLED(409) | 必需 addon 未启用 | 在Admin → Addons打开后重试 |
DEPENDENCY_MISSING(409) | 依赖插件缺失或版本不匹配 | 依赖对话框逐条Download / Update |
DEPENDENCY_CYCLE(409) | 插件依赖成环 | 修正 manifest 依赖声明后重装 |
Update blocked+SIGNATURE_KEY_CHANGED | 作者签名密钥与安装时钉住的不一致 | Review对比两侧指纹 → 经可信渠道向作者核实 →Trust the new key & update |
SIGNATURE_INVALID/SIGNATURE_MISSING/SIGNATURE_INCOMPLETE | 签名无效 / 先签后撤 / 半声明 | 无任何覆盖按钮;不要信任,向作者与注册表报告 |
TREK_VERSION_INCOMPATIBLE/TREK_VERSION_UNKNOWN(409) | 宿主版本超出声明范围 / 未声明范围 | 升级 TREK;后者联系作者补声明 |
NO_COMPATIBLE_UPDATE | 最新兼容版本不比已装版本新 | 无需操作 |
| 健康圆点红(Error) | 子进程启动失败或反复崩溃(5 分钟 5 次上限) | ⋯ → View error log查 stderr 记录;修复后重新启用 |
| 琥珀色Add allowed hostchip | 声明operatorEgress的插件尚无可用主机 | ⋯ → Allowed hosts添加主机;目标在局域网时另设TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on |
| 行标Sideloaded/Dev-Link | 非注册表来源:未签名、未审查、无自动更新 | 视为"只有你背书"的代码;生产上避免长期运行 dev-link |
扩展阅读
- wiki/Plugins.md——插件系统全貌:类型、隔离模型、依赖、活动日志
- wiki/Plugin-Permissions.md——每条权限的确切授予范围与
http:outbound细节 - wiki/Admin-Plugins.md——管理面板逐操作说明
- wiki/Plugin-Development.md——SDK 与 manifest 编写
- wiki/Plugin-Publishing.md——注册表提交流程与
trek-pluginCLI - wiki/Admin-Addons.md——插件可能依赖的 addon 管理
- wiki/Audit-Log.md——实例审计日志与插件能力审计
- wiki/Environment-Variables.md——插件相关环境变量完整参考
- server/src/nest/plugins/——服务端插件模块源码(安装管线、运行时、supervisor、出口策略)
- client/src/components/Admin/AdminPluginsPanel.tsx——管理面板前端实现
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考