pi-information-protecter 0.4.1 → 0.5.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog / 更新日志
2
2
 
3
+ ## 0.5.0 — Actionable block diagnostics / 可定位的拦截诊断
4
+
5
+ - Replace the single generic block message with a stable code, failing stage, reason and suggested action across initialization, configuration, migration, request, scan, worker and audit paths.
6
+ 将单一笼统拦截提示替换为稳定错误码、失败阶段、原因和处理建议,覆盖初始化、配置、迁移、请求、扫描、worker 和审计路径。
7
+ - Report rule and node coordinates, exceeded limits and OS `errno` where known, including codes propagated from both bounded workers.
8
+ 在已知时输出规则及节点定位、超限阈值和系统错误码,包括两个限时 worker 传递的错误码。
9
+ - Reuse the initialization cause for later blocked requests and tool calls, and repeat the last recorded block through `/protecter status`.
10
+ 后续被拦截的请求和工具调用复用初始化原因,并通过状态命令重新展示最近一次拦截。
11
+ - Keep diagnostics free of request text, matched originals, replacement values, configuration content, file paths and raw OS messages; requests still fail closed.
12
+ 诊断不包含请求文本、命中原文、替换值、配置内容、文件路径和原始系统消息;请求仍保持失败不放行。
13
+ - Add diagnostics regressions for sanitization, hostile worker payloads, per-stage codes and rule coordinates.
14
+ 新增诊断回归,覆盖净化、恶意 worker 数据、各阶段错误码和规则定位。
15
+
3
16
  ## 0.4.1 — Safe audit clearing / 安全审计清理
4
17
 
5
18
  - Add `/protecter logs clear`, restricted to local TUI with explicit confirmation and idle waiting; cancellation and non-TUI modes never clear records.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Requires Node.js 22+ and `@earendil-works/pi-coding-agent` **0.85.1–0.85.x**,
14
14
 
15
15
  ```bash
16
16
  # Install the pinned npm release.
17
- pi install npm:pi-information-protecter@0.4.1
17
+ pi install npm:pi-information-protecter@0.5.0
18
18
 
19
19
  # Alternatively, build and install from this repository without copying it.
20
20
  npm ci
@@ -98,6 +98,26 @@ Use `/protecter logs clear` to empty only the current machine's audit log. Local
98
98
 
99
99
  **Logs contain real secrets and reversible mappings.** Unix permissions are 0600; symlinks, hard links and other-user files are rejected. Tool guards cover current/old hash log names and aliases but are not a sandbox. Logs are not encrypted, automatically rotated, migrated with configuration or used to rebuild mappings. Stop all Pi processes before locally archiving/deleting logs or inspecting a stale `.jsonl.lock`. Reading is bounded to the last 1 MiB, display to 100 records and 20,000 characters per preview. A log is capped at 32 MiB and a request batch at 8 MiB; full, unwritable, partial-tail or locked logs block matched requests rather than silently losing audits. Failed scans add no records; a hard crash can leave a partial batch that requires local repair.
100
100
 
101
+ ## Diagnosing a blocked request
102
+
103
+ When a request is blocked, the notification names the failing stage, a stable code and a suggested action instead of one generic message:
104
+
105
+ ```text
106
+ SPI Protecter blocked this request / 已阻止该请求
107
+ code=CONFIG_EMPTY_MATCH stage=configuration rule=1
108
+ Reason / 原因: Regex matches an empty string / 正则匹配空字符串
109
+ Action / 处理: Require a non-empty match in the numbered rule / 修改指定规则,要求非空匹配
110
+ Details / 详情: /protecter status
111
+ ```
112
+
113
+ Stages are `initialization`, `configuration`, `migration`, `request`, `scan`, `worker`, `audit` and `internal`. Coordinates appear only when known: `rule` and `otherRule` are 1-based indexes into `sensitiveWords`, `node` counts scanned payload nodes, `bytes`/`limit`/`timeoutMs` report the exceeded limit, and `errno` is the OS error code.
114
+
115
+ Common codes include `CONFIG_JSON`, `CONFIG_SCHEMA`, `CONFIG_REGEX`, `CONFIG_TARGET`, `CONFIG_CONFLICT`, `CONFIG_EMPTY_MATCH`, `CONFIG_SENSITIVE_TARGET`, `CONFIG_ALIAS_CONFLICT`, `CONFIG_UNSAFE`, `MIGRATION_LOCKED`, `MIGRATION_CHANGED`, `PAYLOAD_JSON`, `PAYLOAD_SIZE`, `SCAN_TIMEOUT`, `SCAN_MATCH_LIMIT`, `SCAN_COMPLEXITY`, `SCAN_ATTACHMENT`, `SCAN_JSON_REWRITE`, `FIXED_OVERLAP`, `FIXED_ALIAS_REUSED`, `AUDIT_FULL`, `AUDIT_PARTIAL`, `AUDIT_LOCKED`, `AUDIT_UNSAFE` and `WORKER_FAILED`.
116
+
117
+ Notifications can scroll away, so `/protecter status` repeats the last recorded block for the session, and a tool blocked while protection is unavailable reports the same cause. Requests still fail closed: the outgoing payload is replaced with `{}`.
118
+
119
+ **Diagnostics never include request text, matched originals, replacement values, configuration content, file paths or raw OS messages.** Only catalog codes, non-negative integer coordinates and an allow-listed `errno` are emitted, so they are safe to paste into an issue. Because matched content is excluded, locating an offending rule may still require local inspection using the reported rule index.
120
+
101
121
  ## How it works
102
122
 
103
123
  ```text
@@ -165,7 +185,7 @@ API references: [Pi Extensions](https://pi.dev/docs/latest/extensions), local 0.
165
185
 
166
186
  ```bash
167
187
  # 安装固定 npm 版本。
168
- pi install npm:pi-information-protecter@0.4.1
188
+ pi install npm:pi-information-protecter@0.5.0
169
189
 
170
190
  # 或在本仓库构建后本地安装,不复制仓库。
171
191
  npm ci
@@ -249,6 +269,26 @@ pi -e ./src/index.ts
249
269
 
250
270
  **日志包含真实秘密及可逆映射。** Unix 权限为 0600,拒绝符号链接、硬链接及他人文件。工具防护覆盖当前和旧机器哈希日志及别名,但不是沙箱。日志不加密、不自动轮转、不随配置迁移,也不用于恢复映射。请停止全部 Pi 进程后再本地归档、删除日志或检查残留 `.jsonl.lock`。读取限最后 1 MiB、最多 100 条,每条预览最多 20,000 字符;单日志上限 32 MiB、请求批次 8 MiB。日志满、不可写、尾部残缺或锁占用时,命中请求会被阻止,不静默漏记。扫描失败不新增记录,硬崩溃可能遗留部分批次,需本地修复。
251
271
 
272
+ ## 拦截原因定位
273
+
274
+ 请求被拦截时,提示不再是单一笼统信息,而是给出失败阶段、稳定错误码和处理建议:
275
+
276
+ ```text
277
+ SPI Protecter blocked this request / 已阻止该请求
278
+ code=CONFIG_EMPTY_MATCH stage=configuration rule=1
279
+ Reason / 原因: Regex matches an empty string / 正则匹配空字符串
280
+ Action / 处理: Require a non-empty match in the numbered rule / 修改指定规则,要求非空匹配
281
+ Details / 详情: /protecter status
282
+ ```
283
+
284
+ 阶段包括初始化、配置、迁移、请求、扇描、worker、审计和内部错误。定位信息仅在已知时输出:`rule` 和 `otherRule` 是敏感词数组中从 1 开始的序号,`node` 为已扫描节点数,`bytes`、`limit`、`timeoutMs` 说明超限情况,`errno` 为系统错误码。
285
+
286
+ 常见错误码包括 `CONFIG_JSON`、`CONFIG_SCHEMA`、`CONFIG_REGEX`、`CONFIG_TARGET`、`CONFIG_CONFLICT`、`CONFIG_EMPTY_MATCH`、`CONFIG_SENSITIVE_TARGET`、`CONFIG_ALIAS_CONFLICT`、`CONFIG_UNSAFE`、`MIGRATION_LOCKED`、`MIGRATION_CHANGED`、`PAYLOAD_JSON`、`PAYLOAD_SIZE`、`SCAN_TIMEOUT`、`SCAN_MATCH_LIMIT`、`SCAN_COMPLEXITY`、`SCAN_ATTACHMENT`、`SCAN_JSON_REWRITE`、`FIXED_OVERLAP`、`FIXED_ALIAS_REUSED`、`AUDIT_FULL`、`AUDIT_PARTIAL`、`AUDIT_LOCKED`、`AUDIT_UNSAFE` 和 `WORKER_FAILED`。
287
+
288
+ 通知可能消失,因此 `/protecter status` 会重新展示本会话最近一次拦截;防护不可用时被拦截的工具也会报告同一原因。请求仍然失败不放行:出站请求体会被替换为 `{}`。
289
+
290
+ **诊断信息不包含请求文本、命中原文、替换值、配置内容、文件路径或原始系统消息。** 仅输出目录内错误码、非负整数定位和白名单 `errno`,因此可安全粘贴到问题报告。也因为不包含命中内容,最终定位问题规则时,仍需根据规则序号在本地查看。
291
+
252
292
  ## 工作方式
253
293
 
254
294
  ```text
package/SECURITY.md CHANGED
@@ -25,6 +25,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
25
25
  日志使用 0600、安全打开检查、协作锁,并在返回脱敏请求前同步落盘;日志失败则拒绝请求。成功表示本地替换而非网关已接收。文件及预览大小受限,用户本地管理保留策略;硬崩溃可能残留锁或不完整记录,同用户权限攻击和终端录屏仍不在防护范围。
26
26
  - `/protecter` previews are TUI-only and require confirmation before showing original values; nothing is injected into model context or session entries. Trusted extensions may still observe UI hooks. Configuration caching remains memory-only during requests, but matched requests now perform audit-file writes.
27
27
  查看器仅限 TUI,展示原文前需确认,不注入模型上下文或会话条目;可信扩展仍可能观察 UI 钩子。出站规则依然使用内存缓存,但命中请求现在会写审计文件。
28
+ - Block diagnostics carry a catalog code, stage, non-negative integer coordinates and an allow-listed OS `errno` only. Rule indexes and node counts are positions, not content. Raw exception messages, request text, matched originals, replacement values, configuration content and file paths are never surfaced, including across the worker boundary, so diagnostics are safe to share. Codes are intentionally coarse, so pinpointing a rule may still need local inspection.
29
+ 拦截诊断仅包含目录错误码、阶段、非负整数定位和白名单系统错误码。规则序号和节点数只是位置,不是内容。不输出原始异常消息、请求文本、命中原文、替换值、配置内容和文件路径,worker 边界同样适用,因此诊断可安全分享;错误码有意保持粗粒度,精确定位规则仍可能需要本地查看。
28
30
  - Limit worker regex scans to 2 seconds, with resource and input limits. Errors exclude original configuration, rules and request text.
29
31
  worker 正则扫描限制为 2 秒,并限制资源和输入;错误不包含原始配置、规则或请求文本。
30
32
  - Pi 0.85.1 catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
package/dist/index.mjs CHANGED
@@ -1,9 +1,13 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{getAgentDir as Ie,VERSION as te}from"@earendil-works/pi-coding-agent";import{Worker as ke}from"node:worker_threads";import{fileURLToPath as ve}from"node:url";import*as u from"node:fs";import{join as v,resolve as fe}from"node:path";import{createHash as ue,randomUUID as le}from"node:crypto";import{Worker as de}from"node:worker_threads";import{fileURLToPath as ge}from"node:url";import{constants as k,closeSync as F,fchmodSync as ne,fstatSync as oe,mkdirSync as ie,openSync as j,readFileSync as se,writeFileSync as ae}from"node:fs";import{dirname as ce}from"node:path";var C={version:1,sensitiveWords:[]},w="SPI Protecter: unsafe or invalid configuration / 配置不安全或格式无效,请在本地修复。";function I(i){try{let e=JSON.parse(i);if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||e.sensitiveWords.length>1e3||Object.keys(e).some(r=>!["version","sensitiveWords"].includes(r)))throw new Error;let n=new Map;for(let r of e.sensitiveWords){if(typeof r=="string"){if(!r||r.length>8192)throw new Error;let t=JSON.stringify(["literal",r]);if(n.has(t)&&n.get(t)!==void 0)throw new Error;n.set(t,void 0);continue}if(!r||typeof r!="object")throw new Error;if(Object.hasOwn(r,"replacement")&&(typeof r.replacement!="string"||!r.replacement.trim()||r.replacement.length>8192||r.replacement.includes("__PIP_")))throw new Error;if(r.type==="literal"){if(Object.keys(r).some(t=>!["type","value","replacement"].includes(t))||typeof r.value!="string"||!r.value||r.value.length>8192)throw new Error}else if(r.type==="regex"){if(Object.keys(r).some(s=>!["type","pattern","flags","replacement"].includes(s))||typeof r.pattern!="string"||!r.pattern||r.pattern.length>8192||r.flags!==void 0&&(typeof r.flags!="string"||!/^[gimsu]*$/.test(r.flags)))throw new Error;let t=new RegExp(r.pattern,r.flags??"")}else throw new Error;let o=r.type==="literal"?JSON.stringify(["literal",r.value]):JSON.stringify(["regex",r.pattern,[...new Set((r.flags??"")+"g")].sort().join("")]);if(n.has(o)&&n.get(o)!==r.replacement)throw new Error;n.set(o,r.replacement)}return e}catch{throw new Error(w)}}function W(i,e=!1){let n;try{if(e){ie(ce(i),{recursive:!0,mode:448});try{let t=j(i,k.O_WRONLY|k.O_CREAT|k.O_EXCL,384);try{ae(t,JSON.stringify(C,null,2)+`
3
- `)}finally{F(t)}}catch(t){if(t.code!=="EEXIST")throw t}}n=j(i,k.O_RDONLY|k.O_NOFOLLOW|k.O_NONBLOCK);let r=oe(n);if(!r.isFile()||r.nlink!==1||r.size>1024*1024||process.getuid&&r.uid!==process.getuid())throw new Error;process.platform!=="win32"&&ne(n,384);let o=se(n,"utf8");if(Buffer.byteLength(o)>1024*1024)throw new Error;return{config:I(o),raw:o}}catch{throw new Error(w)}finally{n!==void 0&&F(n)}}var A=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function me(i){return new Promise((e,n)=>{let r=new de(ge(new URL("./validate-worker.mjs",import.meta.url)),{workerData:i,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),o=!1,t=a=>{o||(o=!0,clearTimeout(s),r.terminate(),a?e():n(new Error(w)))},s=setTimeout(()=>t(!1),2e3);r.once("message",a=>t(a===!0)),r.once("error",()=>t(!1)),r.once("exit",()=>t(!1))})}function pe(i){let e=new Set,n=[];for(let o of i)for(let t of o.sensitiveWords){let s=typeof t=="string"?JSON.stringify(["literal",t,null]):t.type==="literal"?JSON.stringify(["literal",t.value,t.replacement??null]):JSON.stringify(["regex",t.pattern,[...new Set((t.flags??"")+"g")].sort().join(""),t.replacement??null]);e.has(s)||(e.add(s),n.push(t))}let r=JSON.stringify({version:1,sensitiveWords:n});if(Buffer.byteLength(r)>1024*1024)throw new Error(w);return I(r)}function z(i){let e=u.lstatSync(i,{bigint:!0});if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw new Error(w);return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function T(i,e){let n=v(i,e),r=z(n),o=W(n);if(z(n)!==r)throw new Error(w);return{name:e,...o,identity:r,digest:ue("sha256").update(o.raw).digest("hex")}}function N(i,e){let n=T(i,e.name);if(n.identity!==e.identity||n.digest!==e.digest)throw new Error(w)}function R(i){return u.readdirSync(i).filter(e=>A.test(e)).sort()}function U(i){if(process.platform==="win32")return;let e=u.openSync(i,u.constants.O_RDONLY);try{u.fsyncSync(e)}finally{u.closeSync(e)}}async function q(i,e,n={}){if(!/^[a-f0-9]{32}$/.test(e))throw new Error(w);let r=fe(i),o=`protecter.${e}.json`,t=v(r,o);u.mkdirSync(r,{recursive:!0,mode:448});let s=v(r,".protecter-migration.lock"),a=Date.now()+(n.lockTimeoutMs??5e3);for(;;)try{u.mkdirSync(s,{mode:448});break}catch(d){if(d.code!=="EEXIST"||Date.now()>=a)throw new Error("SPI Protecter: migration locked; check active processes / 迁移锁被占用,请检查活动进程。");await new Promise(p=>setTimeout(p,50))}let f;try{let d=R(r);if(d.length>64)throw new Error(w);let l=[...d].sort((h,J)=>h===o?-1:J===o?1:h.localeCompare(J)).map(h=>T(r,h)),g=pe(l.length?l.map(h=>h.config):[C]);if(await me([g]),l.length===1&&l[0].name===o){if(N(r,l[0]),JSON.stringify(R(r))!==JSON.stringify(d))throw new Error(w);return{path:t,config:l[0].config,raw:l[0].raw,sourceRaws:[l[0].raw],migrated:0}}let m=JSON.stringify(g,null,2)+`
4
- `;if(Buffer.byteLength(m)>1024*1024)throw new Error(w);f=v(r,`.protecter-migration.${le()}.tmp`);let y=u.openSync(f,u.constants.O_WRONLY|u.constants.O_CREAT|u.constants.O_EXCL,384);try{u.writeFileSync(y,m),u.fsyncSync(y)}finally{u.closeSync(y)}if(n.checkpoint?.("beforeCommit",t),JSON.stringify(R(r))!==JSON.stringify(d))throw new Error(w);for(let h of l)N(r,h);u.renameSync(f,t),f=void 0,U(r);let E=T(r,o);if(E.raw!==m)throw new Error(w);n.checkpoint?.("afterCommit",t);for(let h of l)h.name!==o&&(n.checkpoint?.("beforeDelete",v(r,h.name)),N(r,E),N(r,h),u.unlinkSync(v(r,h.name)));U(r);let b=[o];if(JSON.stringify(R(r))!==JSON.stringify(b))throw new Error(w);return N(r,E),{path:t,config:g,raw:m,sourceRaws:[...new Set([...l.map(h=>h.raw),m])],migrated:l.filter(h=>h.name!==o).length}}catch{throw new Error(w)}finally{if(f)try{u.unlinkSync(f)}catch{}u.rmdirSync(s)}}import{createHash as he}from"node:crypto";import{readFileSync as we}from"node:fs";import{execFileSync as B}from"node:child_process";var G="SPI Protecter: machine ID unavailable / 无法获取机器标识。";function $(i){let e=i.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw new Error(G);return he("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function H(){try{if(process.platform==="linux")return $(we("/etc/machine-id","utf8"));if(process.platform==="darwin"){let i=B("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return $(i.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let i=B("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return $(i.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw new Error(G)}import*as c from"node:fs";import{randomUUID as ye}from"node:crypto";var O="SPI Protecter: audit unavailable; request blocked / 审计日志不可用,请求已阻止。",Se=32*1024*1024,Ee=8*1024*1024;function Oe(i){let e=n=>String(n).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())} ${e(i.getHours())}:${e(i.getMinutes())}:${e(i.getSeconds())}`}function L(i,e,n=e){let r=c.openSync(i,(e?c.constants.O_RDWR|(n?c.constants.O_CREAT:0):c.constants.O_RDONLY)|c.constants.O_NOFOLLOW|c.constants.O_NONBLOCK,384);try{let o=c.fstatSync(r);if(!o.isFile()||o.nlink!==1||process.getuid&&o.uid!==process.getuid())throw new Error;return process.platform!=="win32"&&c.fchmodSync(r,384),r}catch{throw c.closeSync(r),new Error(O)}}async function X(i){let e=`${i}.lock`,n=Date.now()+2e3;for(;;)try{return c.mkdirSync(e,{mode:448}),e}catch(r){if(r.code!=="EEXIST"||Date.now()>=n)throw new Error(O);await new Promise(o=>setTimeout(o,25))}}async function Y(i){let e=await X(i),n;try{try{n=L(i,!0,!1)}catch(o){if(o.code==="ENOENT")return 0;throw o}let r=c.fstatSync(n).size;return c.ftruncateSync(n,0),c.fsyncSync(n),r}catch{throw new Error(O)}finally{try{n!==void 0&&c.closeSync(n)}finally{c.rmdirSync(e)}}}async function K(i,e,n){if(!n.length)return;let r=new Date,o=ye(),t=Buffer.from(n.map(([f,d])=>JSON.stringify({version:1,time:Oe(r),timezoneOffset:-r.getTimezoneOffset(),requestId:o,provider:e||"unknown",original:d,replacement:f})).join(`
2
+ import{getAgentDir as Ue,VERSION as le}from"@earendil-works/pi-coding-agent";import{Worker as ke}from"node:worker_threads";import{fileURLToPath as be}from"node:url";import*as g from"node:fs";import{join as _,resolve as Oe}from"node:path";import{createHash as we,randomUUID as Ne}from"node:crypto";import{Worker as Ee}from"node:worker_threads";import{fileURLToPath as Se}from"node:url";var W={INTERNAL:["internal","Unexpected local failure / 未预期的本地错误","Reload; check the installed package integrity / 重载并检查安装包完整性"],NOT_READY:["initialization","Protection is not initialized / 防护尚未初始化","Run /protecter status for the initialization error, fix it, then /reload / 查看初始化错误,修复后重载"],UNSUPPORTED_PI:["initialization","Unsupported Pi version / Pi 版本不受支持","Use Pi 0.85.1-0.85.x / 请使用受支持的 Pi 版本"],MACHINE_ID:["initialization","Machine identifier unavailable / 无法获取机器标识","Check local OS machine-ID access; no random fallback is used / 检查系统机器标识访问权限,不使用随机回退"],CONFIG_JSON:["configuration","Invalid configuration JSON / 配置 JSON 无效","Fix JSON syntax locally, then /reload / 本地修复 JSON 语法后重载"],CONFIG_SCHEMA:["configuration","Invalid configuration structure or rule / 配置结构或规则无效","Use version 1, a sensitiveWords array and documented fields; rule shows the 1-based index / 使用版本1、敏感词数组及文档字段,rule 为规则序号"],CONFIG_REGEX:["configuration","Invalid regex or flags / 正则或标志无效","Fix the numbered rule; flags support gimsu only / 修复指定规则,标志仅支持 gimsu"],CONFIG_TARGET:["configuration","Invalid fixed replacement target / 固定替换目标无效","Use a non-blank string under 8192 characters without reserved __PIP_ names / 使用非空且少于8192字符的目标,不含保留名称"],CONFIG_CONFLICT:["configuration","Conflicting rule definitions / 规则定义冲突","Give the same matcher one replacement policy across all configs / 为同一匹配器设置一致的替换策略"],CONFIG_SIZE:["configuration","Configuration exceeds size or rule limit / 配置超过体积或规则数上限","Reduce configuration size or rule count; rules are never truncated / 减少配置体积或规则数,规则不会被截断"],CONFIG_IO:["configuration","Cannot access configuration / 无法访问配置","Check local permissions, ownership and free space / 检查本地权限、所有者和磁盘空间"],CONFIG_UNSAFE:["configuration","Unsafe configuration file / 配置文件不安全","Use an owned regular file without symbolic or hard links / 使用本人所有的普通文件,不含符号或硬链接"],CONFIG_EMPTY_MATCH:["configuration","Regex matches an empty string / 正则匹配空字符串","Require a non-empty match in the numbered rule / 修改指定规则,要求非空匹配"],CONFIG_SENSITIVE_TARGET:["configuration","Fixed target overlaps a sensitive rule / 固定目标与敏感规则重叠","Choose a target unrelated to every sensitive matcher / 选择与所有敏感匹配器无关的目标"],CONFIG_ALIAS_CONFLICT:["configuration","Fixed aliases overlap each other / 固定别名相互重叠","Use distinct aliases that are not substrings of one another / 使用互不包含的不同别名"],CONFIG_TIMEOUT:["configuration","Rule validation timed out / 规则验证超时","Simplify regex rules, then /reload / 简化正则规则后重载"],MIGRATION_LOCKED:["migration","Configuration migration lock is busy / 配置迁移锁被占用","Wait for other Pi processes; inspect a stale lock only after stopping them / 等待其他进程,全部停止后再检查残留锁"],MIGRATION_CHANGED:["migration","Configuration changed during migration / 迁移期间配置发生变化","Stop concurrent edits and /reload; source files are preserved / 停止并发编辑后重载,源文件会保留"],MIGRATION_LIMIT:["migration","Too many configuration candidates / 候选配置过多","Consolidate old configuration files locally / 在本地整理旧配置文件"],MIGRATION_IO:["migration","Migration file operation failed / 迁移文件操作失败","Check permissions, free space and leftover files before retrying / 检查权限、空间和残留文件后重试"],PAYLOAD_JSON:["request","Request is not serializable JSON / 请求无法序列化为 JSON","Inspect the provider or another payload-modifying extension; no payload is logged / 检查提供商或其他请求改写扩展,不记录请求内容"],PAYLOAD_SIZE:["request","Request exceeds the size limit / 请求超过体积上限","Reduce history or tool output size, or start a new session / 减少历史或工具输出,或新建会话"],SCAN_TIMEOUT:["scan","Redaction worker timed out / 脱敏 worker 超时","Simplify regex rules or reduce input size / 简化正则规则或缩小输入"],WORKER_FAILED:["worker","Worker failed to start or exited early / worker 启动失败或提前退出","Verify all dist files and the Node version; reinstall if needed / 检查 dist 文件完整性和 Node 版本,必要时重装"],SCAN_INTERNAL:["scan","Unexpected scan failure / 未预期的扫描错误","Review recent rule changes and retry with smaller input / 检查最近的规则改动并缩小输入"],SCAN_ZERO_WIDTH:["scan","Regex produced a zero-width match / 正则产生零宽匹配","Require a non-empty match in the numbered rule / 修改指定规则,要求非空匹配"],SCAN_MATCH_LIMIT:["scan","Too many matches in one request / 单次请求匹配次数过多","Narrow overly broad rules or reduce input / 缩小过宽规则或减少输入"],SCAN_MAPPING_LIMIT:["scan","Mapping limit reached / 映射数量达到上限","Start a new session or /reload; in-memory mappings reset / 新建会话或重载,内存映射将重置"],FIXED_MAPPING_CONFLICT:["scan","One original has conflicting replacements / 同一原文存在冲突替换","Use one replacement for overlapping matchers, then /reload / 为重叠匹配器使用同一目标后重载"],FIXED_ALIAS_REUSED:["scan","Fixed alias already maps to another original / 固定别名已映射其他原文","A fixed regex represents one original per instance; use random replacement or split literal rules / 固定正则在同一实例仅代表一种原文,改用随机替换或拆分字面规则"],FIXED_SENSITIVE_TARGET:["scan","Replacement contains the matched original / 替换目标包含命中原文","Choose a target without sensitive content / 选择不含敏感内容的目标"],FIXED_OVERLAP:["scan","Fixed replacement partially overlaps another match / 固定替换与其他匹配部分重叠","Remove the partial overlap or use random rules; rule and otherRule show both rules / 消除部分重叠或改用随机规则,rule 与 otherRule 指出两条规则"],FIXED_ALIAS_CROSSING:["scan","Regex crosses a learned fixed alias / 正则跨越已学习的固定别名","Narrow the regex or choose a different alias / 缩小正则范围或更换别名"],SCAN_COMPLEXITY:["scan","Request exceeds depth or node limits / 请求超过层级或节点上限","Reduce nested data and tool schema size / 减少嵌套数据和工具结构体积"],SCAN_JSON_REWRITE:["scan","Replacement breaks embedded JSON syntax / 替换破坏内嵌 JSON 语法","Avoid rules spanning JSON syntax; match field values instead / 避免跨 JSON 语法的规则,改为匹配字段值"],SCAN_ATTACHMENT:["scan","Unsupported image, audio, video or file content / 不支持图片、音频、视频或文件内容","Remove attachments or send reviewed text only / 移除附件或仅发送已审查的文本"],SCAN_KEY_COLLISION:["scan","Replacement creates duplicate object keys / 替换后对象键重复","Use distinct targets or avoid matching schema keys / 使用不同目标或避免匹配结构键"],AUDIT_BATCH_LIMIT:["audit","Audit batch exceeds the size limit / 审计批次超过上限","Reduce matched content per request / 减少单个请求的命中内容"],AUDIT_FULL:["audit","Audit log would exceed the size limit / 审计日志将超过上限","Review records, then run /protecter logs clear or archive locally / 检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["audit","Audit log has an incomplete last line / 审计日志末行不完整","Inspect locally or run /protecter logs clear; never upload the log / 本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["audit","Audit lock stayed busy / 审计锁持续被占用","Wait for writers; stop all Pi processes before inspecting a stale lock / 等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["audit","Unsafe audit file / 审计文件不安全","Use an owned regular file without links; checks are not bypassed / 使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["audit","Audit file operation failed / 审计文件操作失败","Check errno, permissions and free space; no path or content is exposed / 检查错误码、权限和磁盘空间,不暴露路径或内容"]},H=["rule","otherRule","node","bytes","limit","timeoutMs"],ue=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function de(n,e={}){let[t,r,i]=W[n],o={};for(let f of H){let s=e[f];typeof s=="number"&&Number.isSafeInteger(s)&&s>=0&&(o[f]=s)}return typeof e.errno=="string"&&ue.test(e.errno)&&(o.errno=e.errno),{code:n,stage:t,reason:r,action:i,...o}}function S(n){let e=[...H,"errno"].filter(t=>n[t]!==void 0).map(t=>`${t}=${n[t]}`).join(" ");return["SPI Protecter blocked this request / 已阻止该请求",`code=${n.code} stage=${n.stage}${e?` ${e}`:""}`,`Reason / 原因: ${n.reason}`,`Action / 处理: ${n.action}`,"Details / 详情: /protecter status"].join(`
3
+ `)}var N=class extends Error{diagnostic;constructor(e,t={}){let r=de(e,t);super(S(r)),this.name="ProtectionError",this.diagnostic=r}};function c(n,e={}){return new N(n,e)}function y(n,e,t={}){if(n instanceof N)return c(n.diagnostic.code,{...n.diagnostic,...t});let r=n&&typeof n=="object"&&"code"in n?n.code:void 0;return c(e,{...t,errno:typeof r=="string"?r:void 0})}function D(n,e){if(!n||typeof n!="object")return c(e);let t=n,r=typeof t.code=="string"&&Object.hasOwn(W,t.code)?t.code:e;return c(r,t)}import{constants as A,closeSync as j,fchmodSync as ge,fstatSync as me,mkdirSync as he,openSync as Y,readFileSync as pe,writeFileSync as ye}from"node:fs";import{dirname as Ie}from"node:path";var F={version:1,sensitiveWords:[]};function L(n){let e;try{e=JSON.parse(n)}catch{throw c("CONFIG_JSON")}let t=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(i=>!["version","sensitiveWords"].includes(i)))throw c("CONFIG_SCHEMA");if(e.sensitiveWords.length>1e3)throw c("CONFIG_SIZE",{limit:1e3});let r=new Map;for(let i of e.sensitiveWords){if(t++,typeof i=="string"){if(!i||i.length>8192)throw c("CONFIG_SCHEMA",{rule:t,limit:8192});let f=JSON.stringify(["literal",i]);if(r.has(f)&&r.get(f)!==void 0)throw c("CONFIG_CONFLICT",{rule:t});r.set(f,void 0);continue}if(!i||typeof i!="object")throw c("CONFIG_SCHEMA",{rule:t});if(Object.hasOwn(i,"replacement")&&(typeof i.replacement!="string"||!i.replacement.trim()||i.replacement.length>8192||i.replacement.includes("__PIP_")))throw c("CONFIG_TARGET",{rule:t,limit:8192});if(i.type==="literal"){if(Object.keys(i).some(f=>!["type","value","replacement"].includes(f))||typeof i.value!="string"||!i.value||i.value.length>8192)throw c("CONFIG_SCHEMA",{rule:t,limit:8192})}else if(i.type==="regex"){if(Object.keys(i).some(f=>!["type","pattern","flags","replacement"].includes(f))||typeof i.pattern!="string"||!i.pattern||i.pattern.length>8192||i.flags!==void 0&&(typeof i.flags!="string"||!/^[gimsu]*$/.test(i.flags)))throw c("CONFIG_REGEX",{rule:t,limit:8192});try{new RegExp(i.pattern,i.flags??"")}catch{throw c("CONFIG_REGEX",{rule:t})}}else throw c("CONFIG_SCHEMA",{rule:t});let o=i.type==="literal"?JSON.stringify(["literal",i.value]):JSON.stringify(["regex",i.pattern,[...new Set((i.flags??"")+"g")].sort().join("")]);if(r.has(o)&&r.get(o)!==i.replacement)throw c("CONFIG_CONFLICT",{rule:t});r.set(o,i.replacement)}return e}catch(r){throw r instanceof N?r:c("CONFIG_SCHEMA",{rule:t})}}function B(n,e=!1){let t;try{if(e){he(Ie(n),{recursive:!0,mode:448});try{let o=Y(n,A.O_WRONLY|A.O_CREAT|A.O_EXCL,384);try{ye(o,JSON.stringify(F,null,2)+`
4
+ `)}finally{j(o)}}catch(o){if(o.code!=="EEXIST")throw y(o,"CONFIG_IO")}}t=Y(n,A.O_RDONLY|A.O_NOFOLLOW|A.O_NONBLOCK);let r=me(t);if(r.size>1024*1024)throw c("CONFIG_SIZE",{bytes:r.size,limit:1024*1024});if(!r.isFile()||r.nlink!==1||process.getuid&&r.uid!==process.getuid())throw c("CONFIG_UNSAFE");process.platform!=="win32"&&ge(t,384);let i=pe(t,"utf8");if(Buffer.byteLength(i)>1024*1024)throw c("CONFIG_SIZE",{bytes:Buffer.byteLength(i),limit:1024*1024});return{config:L(i),raw:i}}catch(r){let i=r&&typeof r=="object"&&"code"in r?r.code:void 0;throw y(r,i==="ELOOP"||i==="EISDIR"||i==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{t!==void 0&&j(t)}}var G=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function Ae(n){return new Promise((e,t)=>{let r=new Ee(Se(new URL("./validate-worker.mjs",import.meta.url)),{workerData:n,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),i=!1,o=(s,a=c("WORKER_FAILED"))=>{i||(i=!0,clearTimeout(f),r.terminate(),s?e():t(a))},f=setTimeout(()=>o(!1,c("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);r.once("message",s=>s===!0?o(!0):o(!1,D(s,"CONFIG_SCHEMA"))),r.once("error",()=>o(!1)),r.once("exit",()=>o(!1))})}function _e(n){let e=new Set,t=[];for(let i of n)for(let o of i.sensitiveWords){let f=typeof o=="string"?JSON.stringify(["literal",o,null]):o.type==="literal"?JSON.stringify(["literal",o.value,o.replacement??null]):JSON.stringify(["regex",o.pattern,[...new Set((o.flags??"")+"g")].sort().join(""),o.replacement??null]);e.has(f)||(e.add(f),t.push(o))}let r=JSON.stringify({version:1,sensitiveWords:t});if(Buffer.byteLength(r)>1024*1024)throw c("CONFIG_SIZE",{bytes:Buffer.byteLength(r),limit:1024*1024});return L(r)}function q(n){let e;try{e=g.lstatSync(n,{bigint:!0})}catch(t){throw y(t,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw c("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function M(n,e){let t=_(n,e),r=q(t),i=B(t);if(q(t)!==r)throw c("MIGRATION_CHANGED");return{name:e,...i,identity:r,digest:we("sha256").update(i.raw).digest("hex")}}function C(n,e){let t=M(n,e.name);if(t.identity!==e.identity||t.digest!==e.digest)throw c("MIGRATION_CHANGED")}function k(n){return g.readdirSync(n).filter(e=>G.test(e)).sort()}function X(n){if(process.platform==="win32")return;let e=g.openSync(n,g.constants.O_RDONLY);try{g.fsyncSync(e)}finally{g.closeSync(e)}}async function K(n,e,t={}){if(!/^[a-f0-9]{32}$/.test(e))throw c("MACHINE_ID");let r=Oe(n),i=`protecter.${e}.json`,o=_(r,i);g.mkdirSync(r,{recursive:!0,mode:448});let f=_(r,".protecter-migration.lock"),s=Date.now()+(t.lockTimeoutMs??5e3);for(;;)try{g.mkdirSync(f,{mode:448});break}catch(l){if(l.code!=="EEXIST")throw y(l,"MIGRATION_IO");if(Date.now()>=s)throw c("MIGRATION_LOCKED",{timeoutMs:t.lockTimeoutMs??5e3});await new Promise(O=>setTimeout(O,50))}let a;try{let l=k(r);if(l.length>64)throw c("MIGRATION_LIMIT",{limit:64});let u=[...l].sort((h,E)=>h===i?-1:E===i?1:h.localeCompare(E)).map(h=>M(r,h)),p=_e(u.length?u.map(h=>h.config):[F]);if(await Ae([p]),u.length===1&&u[0].name===i){if(C(r,u[0]),JSON.stringify(k(r))!==JSON.stringify(l))throw c("MIGRATION_CHANGED");return{path:o,config:u[0].config,raw:u[0].raw,sourceRaws:[u[0].raw],migrated:0}}let m=JSON.stringify(p,null,2)+`
5
+ `;if(Buffer.byteLength(m)>1024*1024)throw c("CONFIG_SIZE",{bytes:Buffer.byteLength(m),limit:1024*1024});a=_(r,`.protecter-migration.${Ne()}.tmp`);let I=g.openSync(a,g.constants.O_WRONLY|g.constants.O_CREAT|g.constants.O_EXCL,384);try{g.writeFileSync(I,m),g.fsyncSync(I)}finally{g.closeSync(I)}if(t.checkpoint?.("beforeCommit",o),JSON.stringify(k(r))!==JSON.stringify(l))throw c("MIGRATION_CHANGED");for(let h of u)C(r,h);g.renameSync(a,o),a=void 0,X(r);let w=M(r,i);if(w.raw!==m)throw c("MIGRATION_CHANGED");t.checkpoint?.("afterCommit",o);for(let h of u)h.name!==i&&(t.checkpoint?.("beforeDelete",_(r,h.name)),C(r,w),C(r,h),g.unlinkSync(_(r,h.name)));X(r);let R=[i];if(JSON.stringify(k(r))!==JSON.stringify(R))throw c("MIGRATION_CHANGED");return C(r,w),{path:o,config:p,raw:m,sourceRaws:[...new Set([...u.map(h=>h.raw),m])],migrated:u.filter(h=>h.name!==i).length}}catch(l){throw l instanceof N?l:y(l,"MIGRATION_IO")}finally{if(a)try{g.unlinkSync(a)}catch{}g.rmdirSync(f)}}import{createHash as Ce}from"node:crypto";import{readFileSync as Te}from"node:fs";import{execFileSync as Z}from"node:child_process";var V="SPI Protecter: machine ID unavailable / 无法获取机器标识。";function U(n){let e=n.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw new Error(V);return Ce("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function Q(){try{if(process.platform==="linux")return U(Te("/etc/machine-id","utf8"));if(process.platform==="darwin"){let n=Z("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return U(n.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let n=Z("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return U(n.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw new Error(V)}import*as d from"node:fs";import{randomUUID as Re}from"node:crypto";var ee=32*1024*1024,te=8*1024*1024;function De(n){let e=t=>String(t).padStart(2,"0");return`${n.getFullYear()}-${e(n.getMonth()+1)}-${e(n.getDate())} ${e(n.getHours())}:${e(n.getMinutes())}:${e(n.getSeconds())}`}function $(n,e,t=e){let r=d.openSync(n,(e?d.constants.O_RDWR|(t?d.constants.O_CREAT:0):d.constants.O_RDONLY)|d.constants.O_NOFOLLOW|d.constants.O_NONBLOCK,384);try{let i=d.fstatSync(r);if(!i.isFile()||i.nlink!==1||process.getuid&&i.uid!==process.getuid())throw c("AUDIT_UNSAFE");return process.platform!=="win32"&&d.fchmodSync(r,384),r}catch(i){throw d.closeSync(r),y(i,"AUDIT_UNSAFE")}}function b(n){let e=n&&typeof n=="object"&&"code"in n?n.code:void 0;return y(n,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function re(n){let e=`${n}.lock`,t=Date.now()+2e3;for(;;)try{return d.mkdirSync(e,{mode:448}),e}catch(r){if(r.code!=="EEXIST")throw y(r,"AUDIT_IO");if(Date.now()>=t)throw c("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(i=>setTimeout(i,25))}}async function ne(n){let e=await re(n),t;try{try{t=$(n,!0,!1)}catch(i){if(i.code==="ENOENT")return 0;throw i}let r=d.fstatSync(t).size;return d.ftruncateSync(t,0),d.fsyncSync(t),r}catch(r){throw b(r)}finally{try{t!==void 0&&d.closeSync(t)}finally{d.rmdirSync(e)}}}async function ie(n,e,t){if(!t.length)return;let r=new Date,i=Re(),o=Buffer.from(t.map(([a,l])=>JSON.stringify({version:1,time:De(r),timezoneOffset:-r.getTimezoneOffset(),requestId:i,provider:e||"unknown",original:l,replacement:a})).join(`
5
6
  `)+`
6
- `);if(t.length>Ee)throw new Error(O);let s=await X(i),a;try{a=L(i,!0);let f=c.fstatSync(a).size;if(f+t.length>Se)throw new Error;if(f){let d=Buffer.alloc(1);if(c.readSync(a,d,0,1,f-1),d[0]!==10)throw new Error}try{let d=0;for(;d<t.length;){let p=c.writeSync(a,t,d,t.length-d,f+d);if(!p)throw new Error;d+=p}c.fsyncSync(a)}catch{throw c.ftruncateSync(a,f),c.fsyncSync(a),new Error}}catch{throw new Error(O)}finally{a!==void 0&&c.closeSync(a),c.rmdirSync(s)}}function Q(i,e=20){if(!Number.isInteger(e)||e<1||e>100)throw new Error(O);let n;try{try{n=L(i,!1)}catch(g){if(g.code==="ENOENT")return{records:[],truncated:!1};throw g}let r=c.fstatSync(n).size,o=Math.max(0,r-1024*1024),t=Buffer.alloc(r-o),s=c.readSync(n,t,0,t.length,o),a=t.subarray(0,s).toString("utf8");if(o){let g=a.indexOf(`
7
- `);a=g<0?"":a.slice(g+1)}let f=a.split(`
8
- `),d=f.pop()!=="",p=[],l=!1;for(let g of f.slice(-e))try{let m=JSON.parse(g);if(m.version!==1||typeof m.time!="string"||typeof m.provider!="string"||typeof m.original!="string"||typeof m.replacement!="string"||typeof m.requestId!="string"||typeof m.timezoneOffset!="number")throw new Error;p.push(m)}catch{l=!0}return{records:p,truncated:o>0||f.length>e||d||l}}catch{throw new Error(O)}finally{n!==void 0&&c.closeSync(n)}}var S="SPI Protecter: request cleared; check config, audit log, scan limits or attachments / 请求已清空,请检查配置、审计日志、扫描限制或附件。",x=class{constructor(e,n=2e3,r=H){this.configDir=e;this.timeoutMs=n;this.machineId=r}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;snapshot;async initialize(){if(this.closed)throw new Error(S);this.snapshot=void 0;let e=await q(this.configDir,this.machineId());if(this.closed)throw new Error(S);this.snapshot=e}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw new Error(S);return Q(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw new Error(S);return Y(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,n="unknown"){let r=this.queue.then(()=>this.scan(e,n));return this.queue=r.catch(()=>{}),r}async scan(e,n){if(!this.ready)throw new Error(S);let{config:r,sourceRaws:o}=this.snapshot,t=JSON.stringify(e);if(!t||Buffer.byteLength(t)>8*1024*1024)throw new Error(S);let s;try{s=JSON.parse(t)}catch{throw new Error(S)}let a=await new Promise((f,d)=>{let p=!1,l=new ke(ve(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:s,rules:r.sensitiveWords,entries:[...this.tokens],configRaws:o},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}});this.workers.add(l);let g=y=>{if(!p){if(p=!0,clearTimeout(m),this.workers.delete(l),l.terminate(),!y||this.closed){d(new Error(S));return}f(y)}},m=setTimeout(()=>g(),this.timeoutMs);l.once("message",y=>g(y.failed?void 0:y)),l.once("error",()=>g()),l.once("exit",()=>g())});if(this.closed)throw new Error(S);if(await K(this.auditPath,n,a.hits),this.closed)throw new Error(S);for(let[f,d]of a.additions)this.tokens.set(f,d);return a.payload}restoreText(e){let n=t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=[...this.tokens.keys()].filter(t=>!/^__PIP_[a-f0-9]{48}__$/.test(t)),o=new RegExp(["__PIP_[a-f0-9]{48}__",...r.sort((t,s)=>s.length-t.length).map(n)].join("|"),"g");return e.replace(o,t=>this.tokens.get(t)??t)}restore(e,n=0){if(n>80)throw new Error("protecter: 响应层级过深。");if(typeof e=="string")return this.restoreText(e);if(Array.isArray(e))return e.map(r=>this.restore(r,n+1));if(e&&typeof e=="object"){let r=Object.create(null);for(let[o,t]of Object.entries(e)){let s=this.restoreText(o);if(Object.hasOwn(r,s))throw new Error("protecter: 响应键冲突。");r[s]=this.restore(t,n+1)}return r}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as be,statSync as V,readdirSync as Ne}from"node:fs";import{homedir as Pe}from"node:os";import{basename as M,dirname as P,isAbsolute as Re,relative as ee,resolve as _}from"node:path";import{fileURLToPath as xe}from"node:url";function _e(i,e){let n=i.replace(/^@/,"");return n.startsWith("file://")&&(n=xe(n)),(n==="~"||n.startsWith("~/"))&&(n=Pe()+n.slice(1)),_(e,n)}function D(i){try{return be(i)}catch{let e=P(i);return e===i?i:_(D(e),ee(e,i))}}function Ce(i,e){let n=ee(i,e);return n===""||!n.startsWith("..")&&!Re(n)}function Z(i,e){try{let n=V(i),r=V(e);return n.dev===r.dev&&n.ino===r.ino}catch{return!1}}function re(i,e,n,r){if(!r)return!0;let o=D(_(r));if(["bash","powershell"].includes(i)){let a=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(a)||a.includes(r))return!0}let t=e.path??e.file_path??e.filePath,s=["grep","find","ls"].includes(i);if(typeof t!="string"&&!s)return!1;try{let a=_e(typeof t=="string"?t:".",n),f=D(a),d=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,p=Ne(P(o)).filter(l=>d.test(l)).some(l=>Z(a,_(P(o),l)));return P(f)===P(o)&&(A.test(M(f))||d.test(M(f))||M(f).startsWith(".protecter-migration"))||f===o||Z(a,r)||p||s&&Ce(f,o)}catch{return!0}}function Te(i){let e=new x(Ie()),n=/^0\.85\./.test(te)&&Number(te.split(".")[2])>=1,r=!1;i.on("session_start",async(o,t)=>{try{if(!n)throw new Error;await e.initialize(),r=!0;let s=e.ruleCount;t.ui.setStatus("protecter",`SPI Protecter · ${s} rules`),s===0&&t.ui.notify("SPI Protecter: empty rules; edit local machine config and /reload / 规则为空,请编辑本地机器配置后重载。","warning")}catch{r=!1,t.ui.notify(S,"error")}}),i.on("before_provider_request",async(o,t)=>{try{if(!n)throw new Error;let s=await e.redact(o.payload,t.model?.provider??"unknown");return r=!0,s}catch{r=!1;try{await t.abort()}catch{}try{t.ui.notify(S,"error")}catch{}return{}}}),i.on("message_end",o=>{if(o.message.role!=="assistant")return;let t=o.message;return{message:{...t,content:t.content.map(s=>s.type==="text"?{...s,text:e.restoreText(s.text)}:s.type==="thinking"?{...s,thinking:e.restoreText(s.thinking)}:s.type==="toolCall"?{...s,arguments:e.restore(s.arguments)}:s),...t.errorMessage?{errorMessage:e.restoreText(t.errorMessage)}:{}}}}),i.registerMarkdownTransformer((o,t)=>t.messageType==="user"?o:e.restoreText(o)),i.on("tool_call",(o,t)=>{if(!r)return{block:!0,reason:"SPI Protecter 尚未就绪。"};let s=e.restore(o.input);for(let a of Object.keys(o.input))delete o.input[a];if(Object.assign(o.input,s),re(o.toolName,o.input,t.cwd,e.configPath))return{block:!0,reason:"SPI Protecter:禁止工具访问保护配置,请由用户在本地编辑。"}}),i.on("session_before_compact",(o,t)=>(t.ui.notify("SPI Protecter:暂不支持远程上下文压缩,请使用 /new 开始新会话。","warning"),{cancel:!0})),i.on("session_before_tree",o=>o.preparation.userWantsSummary?{cancel:!0}:void 0),i.registerCommand("protecter",{description:"View local protection records / 查看本地保护记录",handler:async(o,t)=>{let s=o.trim().split(/\s+/).filter(Boolean),a=s[0]??"logs";if(!["status","reload","logs"].includes(a)||a!=="logs"&&s.length>1||s.length>2||s[1]!==void 0&&s[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(s[1])){t.ui.notify("Usage / 用法: /protecter [logs [1-100|clear]|status|reload]. No secrets in arguments / 不要在参数中输入秘密。","info");return}if(a==="logs"){if(t.mode!=="tui"){t.ui.notify("Local TUI required / 请在本地 TUI 查看记录。","warning");return}if(s[1]==="clear"){try{if(!e.ready)throw new Error;if(!await t.ui.confirm("Clear current audit log? / 清空当前审计日志?","Irreversible. Only this machine's log; config and mappings stay. Later requests may add records. / 不可撤销,仅清空本机日志,保留配置和映射,后续请求可能新增记录。"))return;await t.waitForIdle();let f=await e.clearAuditRecords();r=e.ready,t.ui.notify(`Audit cleared: ${f} bytes / 已清空日志:${f} 字节。`,"info")}catch{t.ui.notify("Unable to clear audit log; check permissions or lock / 无法清空日志,请检查权限或锁。","error")}return}try{let{records:f,truncated:d}=e.auditRecords(Number(s[1]??20));if(!f.length){t.ui.notify("No readable records / 暂无可读记录。","info");return}let p=E=>JSON.stringify(E).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,b=>`\\u${b.charCodeAt(0).toString(16).padStart(4,"0")}`),l=f.map((E,b)=>`${b+1}. ${p(E.time)} | ${p(E.provider).slice(0,120)}`),g=await t.ui.select(`Protection records / 保护记录${d?" (limited tail / 有限尾部)":""}`,l),m=g===void 0?-1:l.indexOf(g);if(m<0||!await t.ui.confirm("Sensitive plaintext / 敏感明文","Reveal locally? Never share this view / 确认本地显示?请勿分享此界面。"))return;let y=p(f[m]);await t.ui.editor("Local preview; edits discarded / 本地预览,编辑不保存",y.length>2e4?y.slice(0,2e4)+`
9
- [Preview truncated / 预览截断]`:y)}catch{t.ui.notify("Cannot read audit records / 无法读取审计记录。","error")}return}if(a==="reload"){await t.waitForIdle(),await t.reload();return}t.ui.notify(`SPI Protecter: ${e.ready?"ready / 就绪":"not ready / 未就绪"}; ${e.ruleCount} rules / 规则; ${e.mappingCount} mappings / 映射. Memory snapshot; /reload to refresh / 内存快照,重载后更新。`,e.ready?"info":"error")}}),i.on("session_shutdown",()=>e.close())}export{Te as default};
7
+ `);if(o.length>te)throw c("AUDIT_BATCH_LIMIT",{bytes:o.length,limit:te});let f=await re(n),s;try{s=$(n,!0);let a=d.fstatSync(s).size;if(a+o.length>ee)throw c("AUDIT_FULL",{bytes:a+o.length,limit:ee});if(a){let l=Buffer.alloc(1);if(d.readSync(s,l,0,1,a-1),l[0]!==10)throw c("AUDIT_PARTIAL")}try{let l=0;for(;l<o.length;){let O=d.writeSync(s,o,l,o.length-l,a+l);if(!O)throw c("AUDIT_IO");l+=O}d.fsyncSync(s)}catch(l){throw d.ftruncateSync(s,a),d.fsyncSync(s),b(l)}}catch(a){throw b(a)}finally{s!==void 0&&d.closeSync(s),d.rmdirSync(f)}}function oe(n,e=20){if(!Number.isInteger(e)||e<1||e>100)throw c("INTERNAL",{limit:100});let t;try{try{t=$(n,!1)}catch(p){if(p.code==="ENOENT")return{records:[],truncated:!1};throw p}let r=d.fstatSync(t).size,i=Math.max(0,r-1024*1024),o=Buffer.alloc(r-i),f=d.readSync(t,o,0,o.length,i),s=o.subarray(0,f).toString("utf8");if(i){let p=s.indexOf(`
8
+ `);s=p<0?"":s.slice(p+1)}let a=s.split(`
9
+ `),l=a.pop()!=="",O=[],u=!1;for(let p of a.slice(-e))try{let m=JSON.parse(p);if(m.version!==1||typeof m.time!="string"||typeof m.provider!="string"||typeof m.original!="string"||typeof m.replacement!="string"||typeof m.requestId!="string"||typeof m.timezoneOffset!="number")throw new Error;O.push(m)}catch{u=!0}return{records:O,truncated:i>0||a.length>e||l||u}}catch(r){throw b(r)}finally{t!==void 0&&d.closeSync(t)}}var x=class{constructor(e,t=2e3,r=Q){this.configDir=e;this.timeoutMs=t;this.machineId=r}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;snapshot;lastError;get lastDiagnostic(){return this.lastError?{...this.lastError}:void 0}get lastDiagnosticText(){return this.lastError?S(this.lastError):void 0}async initialize(){try{if(this.closed)throw c("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw c("MACHINE_ID")}let t=await K(this.configDir,e);if(this.closed)throw c("NOT_READY");this.snapshot=t,this.lastError=void 0}catch(e){let t=y(e,"CONFIG_IO");throw this.lastError=t.diagnostic,t}}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw this.notReady();return oe(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return ne(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,t="unknown"){let r=this.queue.then(()=>this.scan(e,t)).catch(i=>{let o=y(i,"INTERNAL");throw this.lastError=o.diagnostic,o});return this.queue=r.catch(()=>{}),r}notReady(){return this.lastError?c(this.lastError.code,this.lastError):c("NOT_READY")}async scan(e,t){if(!this.ready)throw this.notReady();let{config:r,sourceRaws:i}=this.snapshot,o;try{o=JSON.stringify(e)}catch{throw c("PAYLOAD_JSON")}if(!o)throw c("PAYLOAD_JSON");if(Buffer.byteLength(o)>8*1024*1024)throw c("PAYLOAD_SIZE",{bytes:Buffer.byteLength(o),limit:8*1024*1024});let f;try{f=JSON.parse(o)}catch{throw c("PAYLOAD_JSON")}let s=await new Promise((a,l)=>{let O=!1,u;try{u=new ke(be(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:f,rules:r.sensitiveWords,entries:[...this.tokens],configRaws:i},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(I){l(y(I,"WORKER_FAILED"));return}this.workers.add(u);let p=(I,w=c("WORKER_FAILED"))=>{if(!O){if(O=!0,clearTimeout(m),this.workers.delete(u),u.terminate(),!I||this.closed){l(this.closed?c("NOT_READY"):w);return}a(I)}},m=setTimeout(()=>p(void 0,c("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);u.once("message",I=>{I?.failed?p(void 0,D(I.diagnostic,"SCAN_INTERNAL")):I&&Array.isArray(I.additions)&&Array.isArray(I.hits)?p(I):p()}),u.once("error",()=>p()),u.once("exit",()=>p())});if(this.closed)throw c("NOT_READY");if(await ie(this.auditPath,t,s.hits),this.closed)throw c("NOT_READY");for(let[a,l]of s.additions)this.tokens.set(a,l);return s.payload}restoreText(e){let t=o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=[...this.tokens.keys()].filter(o=>!/^__PIP_[a-f0-9]{48}__$/.test(o)),i=new RegExp(["__PIP_[a-f0-9]{48}__",...r.sort((o,f)=>f.length-o.length).map(t)].join("|"),"g");return e.replace(i,o=>this.tokens.get(o)??o)}restore(e,t=0){if(t>80)throw new Error("protecter: 响应层级过深。");if(typeof e=="string")return this.restoreText(e);if(Array.isArray(e))return e.map(r=>this.restore(r,t+1));if(e&&typeof e=="object"){let r=Object.create(null);for(let[i,o]of Object.entries(e)){let f=this.restoreText(i);if(Object.hasOwn(r,f))throw new Error("protecter: 响应键冲突。");r[f]=this.restore(o,t+1)}return r}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as xe,statSync as se,readdirSync as ve}from"node:fs";import{homedir as Pe}from"node:os";import{basename as J,dirname as T,isAbsolute as Fe,relative as ce,resolve as v}from"node:path";import{fileURLToPath as Le}from"node:url";function Me(n,e){let t=n.replace(/^@/,"");return t.startsWith("file://")&&(t=Le(t)),(t==="~"||t.startsWith("~/"))&&(t=Pe()+t.slice(1)),v(e,t)}function z(n){try{return xe(n)}catch{let e=T(n);return e===n?n:v(z(e),ce(e,n))}}function Ge(n,e){let t=ce(n,e);return t===""||!t.startsWith("..")&&!Fe(t)}function ae(n,e){try{let t=se(n),r=se(e);return t.dev===r.dev&&t.ino===r.ino}catch{return!1}}function fe(n,e,t,r){if(!r)return!0;let i=z(v(r));if(["bash","powershell"].includes(n)){let s=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(s)||s.includes(r))return!0}let o=e.path??e.file_path??e.filePath,f=["grep","find","ls"].includes(n);if(typeof o!="string"&&!f)return!1;try{let s=Me(typeof o=="string"?o:".",t),a=z(s),l=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,O=ve(T(i)).filter(u=>l.test(u)).some(u=>ae(s,v(T(i),u)));return T(a)===T(i)&&(G.test(J(a))||l.test(J(a))||J(a).startsWith(".protecter-migration"))||a===i||ae(s,r)||O||f&&Ge(a,i)}catch{return!0}}function $e(n){let e=new x(Ue()),t=/^0\.85\./.test(le)&&Number(le.split(".")[2])>=1,r=!1,i,o=f=>{let s=S(f);return i={at:new Date().toISOString(),text:s},s};n.on("session_start",async(f,s)=>{try{if(!t)throw c("UNSUPPORTED_PI");await e.initialize(),r=!0;let a=e.ruleCount;s.ui.setStatus("protecter",`SPI Protecter · ${a} rules`),a===0&&s.ui.notify("SPI Protecter: empty rules; edit local machine config and /reload / 规则为空,请编辑本地机器配置后重载。","warning")}catch(a){r=!1,s.ui.setStatus("protecter","SPI Protecter · not ready / 未就绪"),s.ui.notify(o(y(a,"CONFIG_IO").diagnostic),"error")}}),n.on("before_provider_request",async(f,s)=>{try{if(!t)throw c("UNSUPPORTED_PI");let a=await e.redact(f.payload,s.model?.provider??"unknown");return r=!0,a}catch(a){r=!1;let l=o(y(a,"INTERNAL").diagnostic);try{await s.abort()}catch{}try{s.ui.notify(l,"error")}catch{}return{}}}),n.on("message_end",f=>{if(f.message.role!=="assistant")return;let s=f.message;return{message:{...s,content:s.content.map(a=>a.type==="text"?{...a,text:e.restoreText(a.text)}:a.type==="thinking"?{...a,thinking:e.restoreText(a.thinking)}:a.type==="toolCall"?{...a,arguments:e.restore(a.arguments)}:a),...s.errorMessage?{errorMessage:e.restoreText(s.errorMessage)}:{}}}}),n.registerMarkdownTransformer((f,s)=>s.messageType==="user"?f:e.restoreText(f)),n.on("tool_call",(f,s)=>{if(!r)return{block:!0,reason:e.lastDiagnosticText??i?.text??S(c("NOT_READY").diagnostic)};let a=e.restore(f.input);for(let l of Object.keys(f.input))delete f.input[l];if(Object.assign(f.input,a),fe(f.toolName,f.input,s.cwd,e.configPath))return{block:!0,reason:"SPI Protecter blocked tool access to the protected configuration or audit log; edit it locally / 禁止工具访问受保护的配置或审计日志,请用户在本地编辑。"}}),n.on("session_before_compact",(f,s)=>(s.ui.notify("SPI Protecter:暂不支持远程上下文压缩,请使用 /new 开始新会话。","warning"),{cancel:!0})),n.on("session_before_tree",f=>f.preparation.userWantsSummary?{cancel:!0}:void 0),n.registerCommand("protecter",{description:"View local protection records / 查看本地保护记录",handler:async(f,s)=>{let a=f.trim().split(/\s+/).filter(Boolean),l=a[0]??"logs";if(!["status","reload","logs"].includes(l)||l!=="logs"&&a.length>1||a.length>2||a[1]!==void 0&&a[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(a[1])){s.ui.notify("Usage / 用法: /protecter [logs [1-100|clear]|status|reload]. No secrets in arguments / 不要在参数中输入秘密。","info");return}if(l==="logs"){if(s.mode!=="tui"){s.ui.notify("Local TUI required / 请在本地 TUI 查看记录。","warning");return}if(a[1]==="clear"){try{if(!e.ready)throw new Error;if(!await s.ui.confirm("Clear current audit log? / 清空当前审计日志?","Irreversible. Only this machine's log; config and mappings stay. Later requests may add records. / 不可撤销,仅清空本机日志,保留配置和映射,后续请求可能新增记录。"))return;await s.waitForIdle();let u=await e.clearAuditRecords();r=e.ready,s.ui.notify(`Audit cleared: ${u} bytes / 已清空日志:${u} 字节。`,"info")}catch(u){s.ui.notify(o(y(u,"AUDIT_IO").diagnostic),"error")}return}try{let{records:u,truncated:p}=e.auditRecords(Number(a[1]??20));if(!u.length){s.ui.notify("No readable records / 暂无可读记录。","info");return}let m=E=>JSON.stringify(E).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,P=>`\\u${P.charCodeAt(0).toString(16).padStart(4,"0")}`),I=u.map((E,P)=>`${P+1}. ${m(E.time)} | ${m(E.provider).slice(0,120)}`),w=await s.ui.select(`Protection records / 保护记录${p?" (limited tail / 有限尾部)":""}`,I),R=w===void 0?-1:I.indexOf(w);if(R<0||!await s.ui.confirm("Sensitive plaintext / 敏感明文","Reveal locally? Never share this view / 确认本地显示?请勿分享此界面。"))return;let h=m(u[R]);await s.ui.editor("Local preview; edits discarded / 本地预览,编辑不保存",h.length>2e4?h.slice(0,2e4)+`
10
+ [Preview truncated / 预览截断]`:h)}catch(u){s.ui.notify(o(y(u,"AUDIT_IO").diagnostic),"error")}return}if(l==="reload"){await s.waitForIdle(),await s.reload();return}let O=e.lastDiagnosticText??i?.text;s.ui.notify([`SPI Protecter: ${e.ready?"ready / 就绪":"not ready / 未就绪"}; ${e.ruleCount} rules / 规则; ${e.mappingCount} mappings / 映射. Memory snapshot; /reload to refresh / 内存快照,重载后更新。`,O?`
11
+ Last block / 最近拦截${i?` @ ${i.at}`:""}:
12
+ ${O}`:`
13
+ No recorded block in this session / 本会话暂无拦截记录。`].join(""),e.ready?"info":"error")}}),n.on("session_shutdown",()=>e.close())}export{$e as default};
@@ -1,2 +1,2 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{parentPort as b,workerData as T}from"node:worker_threads";import{randomBytes as $}from"node:crypto";try{let S=function(t,r){let e=p.get(t);if(e){if(r!==void 0&&e!==r)throw new Error;return e}if(r!==void 0){if(c.size>=5e4)throw new Error;if(c.has(r)&&c.get(r)!==t)throw new Error;if(r.includes(t))throw new Error;return p.set(t,r),c.set(r,t),u.push([r,t]),r}if(c.size>=5e4)throw new Error;do e=`__PIP_${$(24).toString("hex")}__`;while(c.has(e)||N.includes(e));return p.set(t,e),c.set(e,t),u.push([e,t]),e},m=function(t){let r=[];for(let o of w)if(o.literal===void 0){o.regex.lastIndex=0;for(let n of t.matchAll(o.regex)){if(!n[0].length)throw new Error;if(r.push([n.index,n.index+n[0].length,o.replacement]),++y>1e5)throw new Error}}else{if(!o.literal)continue;let n=0;for(;(n=t.indexOf(o.literal,n))!==-1;){if(r.push([n,n+o.literal.length,o.replacement]),++y>1e5)throw new Error;n++}}r.sort((o,n)=>o[0]-n[0]||n[1]-o[1]);let e=[];for(let o of r){let n=e.at(-1);if(n&&o[0]<n[1]){if(n[2]!==void 0||o[2]!==void 0){if(n[0]!==o[0]||n[1]!==o[1]||n[2]!==void 0&&o[2]!==void 0&&n[2]!==o[2])throw new Error;n[2]??=o[2]}n[1]=Math.max(n[1],o[1])}else e.push([...o])}let s="",f=0;for(let[o,n,i]of e){let a=S(t.slice(o,n),i);x.add(a),s+=t.slice(f,o)+a,f=n}return s+t.slice(f)},h=function(t){let r="",e=0,s=i=>i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),f=[...c.keys()].filter(i=>!/^__PIP_[a-f0-9]{48}__$/.test(i)),o=new RegExp(["__PIP_[a-f0-9]{48}__",...f.sort((i,a)=>a.length-i.length).map(s)].join("|"),"g"),n=[...t.matchAll(o)].filter(i=>c.has(i[0]));for(let i of w)if(!(!i.regex||!f.length)){i.regex.lastIndex=0;for(let a of t.matchAll(i.regex)){if(++y>1e5||!a[0].length)throw new Error;if(n.some(g=>f.includes(g[0])&&a.index<g.index+g[0].length&&a.index+a[0].length>g.index))throw new Error}}for(let i of n)c.has(i[0])&&(r+=m(t.slice(e,i.index))+i[0],e=i.index+i[0].length);return r+m(t.slice(e))},d=function(t,r=0){if(++M>2e5||r>80)throw new Error;if(typeof t=="string"){if(O.some(e=>t.includes(e)))return h(t);if(/^\s*[[{]/.test(t)){let e;try{e=JSON.parse(t)}catch{}if(e&&typeof e=="object"){let s=JSON.stringify(d(e,r+1)),f=h(s);return f===s?s:(JSON.parse(f),f)}}return h(t)}if(typeof t=="number"){let e=h(String(t));return e===String(t)?t:e}if(Array.isArray(t))return t.map(e=>d(e,r+1));if(t&&typeof t=="object"){if(typeof t.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(t.type))throw new Error;if(Object.keys(t).some(s=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(s)))throw new Error;let e=Object.create(null);for(let[s,f]of Object.entries(t)){let o=h(s);if(Object.hasOwn(e,o))throw new Error;e[o]=d(f,r+1)}return e}if(t===null||typeof t=="boolean")return t;throw new Error},l=function(t,r,e=0){if(e>80)throw new Error;if(typeof t=="string"){if(t.includes(r))return!0;let s;try{s=JSON.parse(t)}catch{return!1}return s&&typeof s=="object"?l(s,r,e+1):!1}return Array.isArray(t)?t.some(s=>l(s,r,e+1)):t&&typeof t=="object"?Object.entries(t).some(([s,f])=>s.includes(r)||l(f,r,e+1)):!1};I=S,k=m,D=h,R=d,z=l;let{payload:_,rules:A,entries:E,configRaws:J}=T,p=new Map(E.map(([t,r])=>[r,t])),c=new Map(E),u=[],x=new Set,w=A.map(t=>typeof t=="string"?{literal:t}:t.type==="literal"?{literal:t.value,replacement:t.replacement}:{replacement:t.replacement,regex:new RegExp(t.pattern,[...new Set((t.flags??"")+"g")].join(""))});for(let t of p.keys())w.push({literal:t});let O=[...new Set(J.flatMap(t=>[t,JSON.stringify(JSON.parse(t))]))];for(let t of O)w.push({literal:t});let N=JSON.stringify(_),y=0,M=0,j=d(_),P=[...x].filter(t=>l(j,t)).map(t=>[t,c.get(t)]);b.postMessage({payload:j,additions:u,hits:P})}catch{b.postMessage({failed:!0})}var I,k,D,R,z;
2
+ import{parentPort as L,workerData as w}from"node:worker_threads";import{randomBytes as b}from"node:crypto";var u,T=0;function f(l,I){throw{code:l,rule:u,otherRule:I,node:T}}try{let x=function(e,r){let t=g.get(e);if(t)return r!==void 0&&t!==r&&f("FIXED_MAPPING_CONFLICT"),t;if(r!==void 0)return d.size>=5e4&&f("SCAN_MAPPING_LIMIT"),d.has(r)&&d.get(r)!==e&&f("FIXED_ALIAS_REUSED"),r.includes(e)&&f("FIXED_SENSITIVE_TARGET"),g.set(e,r),d.set(r,e),N.push([r,e]),r;d.size>=5e4&&f("SCAN_MAPPING_LIMIT");do t=`__PIP_${b(24).toString("hex")}__`;while(d.has(t)||D.includes(t));return g.set(e,t),d.set(t,e),N.push([t,e]),t},m=function(e){let r=[];for(let n of S)if(u=n.rule,n.literal===void 0){n.regex.lastIndex=0;for(let i of e.matchAll(n.regex))i[0].length||f("SCAN_ZERO_WIDTH"),r.push([i.index,i.index+i[0].length,n.replacement,n.rule]),++y>1e5&&f("SCAN_MATCH_LIMIT")}else{if(!n.literal)continue;let i=0;for(;(i=e.indexOf(n.literal,i))!==-1;)r.push([i,i+n.literal.length,n.replacement,n.rule]),++y>1e5&&f("SCAN_MATCH_LIMIT"),i++}r.sort((n,i)=>n[0]-i[0]||i[1]-n[1]);let t=[];for(let n of r){let i=t.at(-1);i&&n[0]<i[1]?((i[2]!==void 0||n[2]!==void 0)&&((i[0]!==n[0]||i[1]!==n[1]||i[2]!==void 0&&n[2]!==void 0&&i[2]!==n[2])&&(u=i[3],f("FIXED_OVERLAP",n[3])),i[2]===void 0&&n[2]!==void 0&&(i[3]=n[3]),i[2]??=n[2]),i[1]=Math.max(i[1],n[1])):t.push([...n])}let s="",c=0;for(let[n,i,o,a]of t){u=a;let _=x(e.slice(n,i),o);C.add(_),s+=e.slice(c,n)+_,c=i}return s+e.slice(c)},p=function(e){let r="",t=0,s=o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=[...d.keys()].filter(o=>!/^__PIP_[a-f0-9]{48}__$/.test(o)),n=new RegExp(["__PIP_[a-f0-9]{48}__",...c.sort((o,a)=>a.length-o.length).map(s)].join("|"),"g"),i=[...e.matchAll(n)].filter(o=>d.has(o[0]));for(let o of S)if(!(!o.regex||!c.length)){u=o.rule,o.regex.lastIndex=0;for(let a of e.matchAll(o.regex))++y>1e5&&f("SCAN_MATCH_LIMIT"),a[0].length||f("SCAN_ZERO_WIDTH"),i.some(_=>c.includes(_[0])&&a.index<_.index+_[0].length&&a.index+a[0].length>_.index)&&f("FIXED_ALIAS_CROSSING")}for(let o of i)d.has(o[0])&&(r+=m(e.slice(t,o.index))+o[0],t=o.index+o[0].length);return r+m(e.slice(t))},A=function(e,r=0){if(T=++M,u=void 0,(M>2e5||r>80)&&f("SCAN_COMPLEXITY"),typeof e=="string"){if(E.some(t=>e.includes(t)))return p(e);if(/^\s*[[{]/.test(e)){let t;try{t=JSON.parse(e)}catch{}if(t&&typeof t=="object"){let s=JSON.stringify(A(t,r+1)),c=p(s);if(c===s)return s;try{JSON.parse(c)}catch{f("SCAN_JSON_REWRITE")}return c}}return p(e)}if(typeof e=="number"){let t=p(String(e));return t===String(e)?e:t}if(Array.isArray(e))return e.map(t=>A(t,r+1));if(e&&typeof e=="object"){typeof e.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(e.type)&&f("SCAN_ATTACHMENT"),Object.keys(e).some(s=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(s))&&f("SCAN_ATTACHMENT");let t=Object.create(null);for(let[s,c]of Object.entries(e)){let n=p(s);Object.hasOwn(t,n)&&f("SCAN_KEY_COLLISION"),t[n]=A(c,r+1)}return t}if(e===null||typeof e=="boolean")return e;f("PAYLOAD_JSON")},h=function(e,r,t=0){if(t>80&&f("SCAN_COMPLEXITY"),typeof e=="string"){if(e.includes(r))return!0;let s;try{s=JSON.parse(e)}catch{return!1}return s&&typeof s=="object"?h(s,r,t+1):!1}return Array.isArray(e)?e.some(s=>h(s,r,t+1)):e&&typeof e=="object"?Object.entries(e).some(([s,c])=>s.includes(r)||h(c,r,t+1)):!1};J=x,H=m,X=p,$=A,F=h;let{payload:l,rules:I,entries:O,configRaws:R}=w,g=new Map(O.map(([e,r])=>[r,e])),d=new Map(O),N=[],C=new Set,S=I.map((e,r)=>{let t=r+1;return typeof e=="string"?{literal:e,rule:t}:e.type==="literal"?{literal:e.value,replacement:e.replacement,rule:t}:{rule:t,replacement:e.replacement,regex:new RegExp(e.pattern,[...new Set((e.flags??"")+"g")].join(""))}});for(let e of g.keys())S.push({literal:e});let E=[...new Set(R.flatMap(e=>[e,JSON.stringify(JSON.parse(e))]))];for(let e of E)S.push({literal:e});let D=JSON.stringify(l),y=0,M=0,P=A(l),j=[...C].filter(e=>h(P,e)).map(e=>[e,d.get(e)]);L.postMessage({payload:P,additions:N,hits:j})}catch(l){L.postMessage({failed:!0,diagnostic:{code:typeof l?.code=="string"?l.code:"SCAN_INTERNAL",rule:l?.rule??u,otherRule:l?.otherRule,node:l?.node??T}})}var J,H,X,$,F;
@@ -1,2 +1,2 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{parentPort as i,workerData as c}from"node:worker_threads";try{for(let f of c){let n=f.sensitiveWords,o=n.filter(e=>typeof e=="object"&&e.replacement!==void 0);for(let e of n)if(typeof e=="object"&&e.type==="regex"&&new RegExp(e.pattern,e.flags??"").test(""))throw new Error;for(let e=0;e<o.length;e++){let r=o[e].replacement;for(let t of n)if(typeof t=="string"?r.includes(t)||t.includes(r):t.type==="literal"?r.includes(t.value)||t.value.includes(r):new RegExp(t.pattern,t.flags??"").test(r))throw new Error;for(let t=0;t<e;t++){let s=o[t];if(!r.includes(s.replacement)&&!s.replacement.includes(r))continue;if(!(JSON.stringify(o[e])===JSON.stringify(s)))throw new Error}}}i.postMessage(!0)}catch{i.postMessage(!1)}
2
+ import{parentPort as a,workerData as d}from"node:worker_threads";var i;function u(n,c){throw{code:n,rule:i,otherRule:c}}try{for(let n of d){let s=n.sensitiveWords.map((e,r)=>({rule:e,index:r+1})),l=s.filter(e=>typeof e.rule=="object"&&e.rule.replacement!==void 0);for(let e of s)i=e.index,typeof e.rule=="object"&&e.rule.type==="regex"&&new RegExp(e.rule.pattern,e.rule.flags??"").test("")&&u("CONFIG_EMPTY_MATCH");for(let e=0;e<l.length;e++){let r=l[e].rule.replacement;i=l[e].index;for(let o of s){let t=o.rule;(typeof t=="string"?r.includes(t)||t.includes(r):t.type==="literal"?r.includes(t.value)||t.value.includes(r):new RegExp(t.pattern,t.flags??"").test(r))&&u("CONFIG_SENSITIVE_TARGET",o.index)}for(let o=0;o<e;o++){let t=l[o],f=t.rule.replacement;!r.includes(f)&&!f.includes(r)||JSON.stringify(l[e].rule)!==JSON.stringify(t.rule)&&u("CONFIG_ALIAS_CONFLICT",t.index)}}}a.postMessage(!0)}catch(n){a.postMessage({code:typeof n?.code=="string"?n.code:"CONFIG_SCHEMA",rule:n?.rule??i,otherRule:n?.otherRule})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-information-protecter",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",