@shinsou/plugin 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aluo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,336 @@
1
- # Temporary Holding Version
1
+ # @shinsou/plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shinsou X 套件作者工具(指令 `shinsou-plugin`):產生發佈者金鑰、建立套件專案、打包、靜態檢查、
4
+ 用錄製的 HTTP 回應在本機沙箱跑 hook、開發者用即時伺服器,以及簽章發佈、撤回與金鑰輪替。
5
+
6
+ Shinsou X **沒有「官方」套件**:每個套件都是第三方套件,由作者自己的 Ed25519 發佈者金鑰簽章,
7
+ 在 App 的沙箱中執行。不同發佈者可以發佈相同的 `packageId`,App 會把它們當成不同的套件。
8
+
9
+ > 本工具產生的簽章格式與 App 逐位元組一致:`test/fixtures/contract-v1/` 是 App 產生的共用測試向量,
10
+ > `npm test` 會重現每一個有效 envelope、拒絕每一個無效 envelope,並逐 frame 比對 SandboxProtocol v1 的 CBOR。
11
+
12
+ ## 安裝
13
+
14
+ 需要 Node.js 20 以上。
15
+
16
+ ```bash
17
+ npm install --save-dev @shinsou/plugin
18
+ ```
19
+
20
+ 或全域安裝:
21
+
22
+ ```bash
23
+ npm install --global @shinsou/plugin
24
+ ```
25
+
26
+ > 目前尚未發佈到 npm,請直接從 GitHub 安裝(安裝時會自動建置):
27
+
28
+ ```bash
29
+ npm install --global github:aluo96078/shinsou-plugin-cli
30
+ ```
31
+
32
+ 或固定在某個版本(commit 或 tag):
33
+
34
+ ```bash
35
+ npm install --save-dev github:aluo96078/shinsou-plugin-cli#<commit>
36
+ ```
37
+
38
+ 從原始碼開發:
39
+
40
+ ```bash
41
+ git clone https://github.com/aluo96078/shinsou-plugin-cli && cd shinsou-plugin-cli && npm install && npm run build && npm link
42
+ ```
43
+
44
+ ## 完整流程
45
+
46
+ ```text
47
+ keygen → init → build → lint → test → serve(App 開發者模式試用)→ sign → 發佈
48
+ ↘ 之後:更新版本 → sign;revoke/refresh/rotate/revoke-key
49
+ ```
50
+
51
+ ### 1. 產生發佈者金鑰:`keygen`
52
+
53
+ ```bash
54
+ shinsou-plugin keygen --name mykey
55
+ ```
56
+
57
+ - 種子(私鑰)存在 `~/.config/shinsou-plugin/keys/mykey.json`,權限 `0600`(目錄 `0700`)。
58
+ 可用 `SHINSOU_PLUGIN_HOME` 改位置。
59
+ - 互動模式會詢問密語;輸入密語後種子以 **scrypt(N=2^17, r=8, p=1)+ XChaCha20-Poly1305** 加密,
60
+ 指紋作為 AEAD 附加資料。加 `--passphrase` 表示一定要設定密語;非互動環境可用 `SHINSOU_PLUGIN_PASSPHRASE`。
61
+ - 會印出兩種指紋:
62
+ - **完整指紋**:`sha256:` + SHA-256(公鑰) 的無 padding base64url。比對一律用這個。
63
+ - **顯示指紋**:SHA-256(公鑰) 前 16 bytes,大寫十六進位、每 4 字元一組(例如 `20C7 D064 6108 …`)。
64
+ - 請把指紋公布在你的網站,使用者可以用 `shinsou://add-repository?url=…&fp=<完整指紋>` 連結加入並自動比對。
65
+ - **備份金鑰檔**。遺失私鑰無法自動恢復,只能發佈新金鑰並請使用者手動改信任。
66
+
67
+ `shinsou-plugin fingerprint --key mykey` 可隨時再看指紋。
68
+
69
+ ### 2. 建立專案:`init`
70
+
71
+ ```bash
72
+ shinsou-plugin init my-plugin --template manga # manga | novel | mixed | async
73
+ ```
74
+
75
+ | 範本 | 內容 |
76
+ |---|---|
77
+ | `manga` | 以 `bridge.httpGet` + `Jsoup` 解析 HTML 的漫畫來源,回傳圖片頁 |
78
+ | `novel` | 小說來源(`PLAIN_TEXT`):定義 `getChapterText(chapter)`,直接回傳章節文字字串(上限 4 MiB) |
79
+ | `mixed` | 一個套件兩個來源:漫畫來源(`IMAGE_SEQUENCE`,`getPageList`)+小說來源(`PLAIN_TEXT`,`getChapterText`),以各來源自己的 `contentKinds` 區分;套件層 `contentType` 為 `both`。`init` 會把產生的 `sourceId` 填進 `var sources = [...]` 的 `id` 與測試案例 |
80
+ | `async` | JSON API 來源:`async` hook、非阻塞的 `bridge.net.fetch`、多檔 ES module(打包成單一腳本) |
81
+
82
+ 產生的檔案:
83
+
84
+ ```text
85
+ shinsou.plugin.json ← 唯一的事實來源:index、sidecar、腳本內的 v2 宣告都由它產生
86
+ src/index.js ← 進入點,必須在頂層宣告 var source = {...}(多來源用 var sources = [...])
87
+ test/cases.json ← hook 測試案例
88
+ test/fixtures/*.json ← 錄製的 HTTP 回應
89
+ .github/workflows/shinsou-refresh.yml ← 定期重簽撤回清單
90
+ ```
91
+
92
+ #### `shinsou.plugin.json`
93
+
94
+ ```jsonc
95
+ {
96
+ "manifestVersion": 1,
97
+ "packageId": "zh.example", // 小寫 [a-z0-9._-]
98
+ "name": "範例漫畫",
99
+ "version": "1.0.0",
100
+ "versionCode": 1, // 每次發佈都必須遞增;App 拒絕降版
101
+ "lang": "zh",
102
+ "nsfw": false,
103
+ "contract": "shinsou",
104
+ "runtime": "legacy-shinsou-adapter-v2",
105
+ "contentType": "manga", // manga | novel | both
106
+ "contentKinds": ["IMAGE_SEQUENCE"], // IMAGE_SEQUENCE | PLAIN_TEXT | EPUB_SPINE
107
+ "capabilities": ["CATALOGUE", "LATEST", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
108
+ "runtimePermissions": ["EXECUTE_SCRIPT", "NETWORK"],
109
+ "requestedHostPermissions": [],
110
+ "systemEvents": { "required": [], "optional": [] },
111
+ "entry": "src/index.js",
112
+ "sources": [{
113
+ "sourceId": "1234567890123456789", // 十進位 64-bit 整數字串(init 會自動產生)
114
+ "legacyLongId": null,
115
+ "name": "範例漫畫",
116
+ "lang": "zh",
117
+ "baseUrl": "https://example.com",
118
+ "requestOrigins": ["https://example.com"], // 腳本能連線的 exact HTTPS origin
119
+ "credentialOrigins": [], // Host 代管 Cookie 的 origin(必須也是 request origin)
120
+ "contentOrigins": ["https://img.example.com"],
121
+ "browserSessionOrigins": [], // 最多 4 個;需要 BROWSER_CHALLENGE
122
+ "contentHostSuffixes": [] // 敏感:動態 CDN 後綴(見「已知限制」)
123
+ }],
124
+ "repository": {
125
+ "name": "我的套件", // 發佈者名稱(App 顯示為「未驗證」名稱)
126
+ "shortName": "我的套件",
127
+ "website": "https://github.com/you/plugins",
128
+ "mirrors": ["https://you.github.io/plugins"] // 每個發佈網址;每個都會各簽一份
129
+ },
130
+ "test": { "cases": "test/cases.json", "fixtures": "test/fixtures" }
131
+ }
132
+ ```
133
+
134
+ ### 3. 打包:`build`
135
+
136
+ ```bash
137
+ shinsou-plugin build
138
+ ```
139
+
140
+ - 用 esbuild 把進入點(可 `import` 其他檔案)打包成**單一 classic script**,目標 ES2020。
141
+ - 在檔頭寫入一行 `var __shinsouExtensionV2 = {...};`,檔尾寫入 `source.v2 = __shinsouExtensionV2`。
142
+ 若原始碼已有自己的宣告(從舊套件遷移),必須與 `shinsou.plugin.json` 完全一致,否則失敗。
143
+ - 計算 SHA-256 與 byte size,產生 `index.json` 條目與 `sidecars/<id>.json`
144
+ (`shinsou-extension-sidecar-v2`,`contractVersion` 2)。
145
+ - 把寫出的檔案重新讀回,以社群儲存庫 `test/v2-migration-smoke.js` 的通用規則檢查
146
+ 腳本宣告/index/sidecar 三方一致(版本、內容類型、capability、事件、權限、來源與 origin 綁定、digest);
147
+ 任何不一致都會讓 build 失敗。
148
+ - 輸出到 `build/repo/`(未簽章):
149
+
150
+ ```text
151
+ build/repo/index.json
152
+ build/repo/repo.json
153
+ build/repo/plugins/<id>.js
154
+ build/repo/sidecars/<id>.json
155
+ ```
156
+
157
+ ### 4. 靜態檢查:`lint`
158
+
159
+ ```bash
160
+ shinsou-plugin lint
161
+ ```
162
+
163
+ 錯誤(exit code 1):
164
+
165
+ - **誤導性名稱**:packageId、套件名稱、來源名稱、發佈者名稱(`repository.name`/`shortName`)不得含
166
+ 「官方」、「官網」、「公式」、`official`(含 `offical`、`oficial` 等變體)。比對不分大小寫、全形/半形、
167
+ 重音符號、零寬字元、常見的數字替代(`0fficial`、`offic1al`),並忽略 `_ - . 空白` 分隔符號,
168
+ 所以 `shinsou_official`、`Shinsou.Official`、`o.f.f.i.c.i.a.l`、`官 方` 都會被拒絕。
169
+ - **權限涵蓋 bridge op**:掃描腳本用到的 `bridge.*`,例如 `httpGet`/`net.fetch` → `NETWORK`、
170
+ `setCookie` → `COOKIE_STORAGE`、`getCookie` → `COOKIE_READ`、`getCredential*` → `CREDENTIAL_ACCESS`、
171
+ `browserSessionRequest` → `BROWSER_CHALLENGE`、`bridge.system.requestLogin` → `REQUEST_LOGIN_UI` + 登入事件。
172
+ 不存在的 op(沙箱會回 `UNKNOWN_OPERATION`)也算錯誤。
173
+ - **origin 必須是 exact HTTPS**:沒有路徑、沒有多餘的預設埠、不是 IP、localhost、`.local` 等私有網路主機。
174
+ `credentialOrigins` 必須也在 `requestOrigins` 內;宣告 `NETWORK` 但 `requestOrigins` 是空的也算錯
175
+ (App 會拒絕所有請求)。
176
+ - **腳本 ≤ 2 MiB**、`sourceId` 是 64-bit 整數字串、`REPORT_USER_MESSAGE` 等 tier ceiling 權限不能申請。
177
+ - `contentHostSuffixes` 規則與 App 相同:小寫 ASCII、不含前後的點、不是 IP,且依**完整 Public Suffix List**
178
+ 必須是可註冊網域(`cdn.example.com`、`example.com`、`user.github.io` 可以;`com`、`co.jp`、`github.io` 不行)。
179
+ 可在後面加上剛好是 `:1024-65535` 的後綴(例如 `hath.network:1024-65535`)以允許非特權連接埠,其他連接埠寫法一律拒絕;
180
+ 每個來源最多 8 個。清單固定版本放在 `data/public_suffix_list.dat`(與 App 內建的同一份,來源 commit 與 SHA-256 見 `data/README.md`)。
181
+ - `getCookie`/`getCookies` 需要同時宣告 `COOKIE_STORAGE` 與 `COOKIE_READ`(App 已實作)。
182
+ - 名稱可使用明確的否認字樣(`Unofficial`、`非官方`、`非公式`);移除這些字樣後仍含有 official/官方/公式 的名稱照樣被拒絕。
183
+ - `PLAIN_TEXT` 來源應定義 `getChapterText`;`shinsou-plugin test` 對 `getChapterText` 的測試案例會檢查回傳是非空字串(或 `{ text }`)且不超過 4 MiB,
184
+ 並可用 `expect.equals`/`expect.contains` 比對文字。
185
+
186
+ 警告:宣告了卻沒用到的權限(最小權限)、宣告了 capability 卻找不到對應 hook、腳本中出現未宣告的網址等。
187
+
188
+ ### 5. 本機測試:`test`
189
+
190
+ ```bash
191
+ shinsou-plugin test # 以 test/fixtures 重播
192
+ shinsou-plugin test --record # 缺少的回應實際連線抓取並存成 fixture,再重跑
193
+ shinsou-plugin test --update # 更新 snapshot
194
+ shinsou-plugin test --only pages
195
+ ```
196
+
197
+ - Hook 在 **QuickJS WASM 沙箱**(quickjs-emscripten)中執行,並載入與 App 相同的 JavaScript bridge bootstrap
198
+ (`Jsoup`、`SManga`、`SChapter`、`Page`、`MangasPage`、`bridge.*`),另外提供 `bridge.net.fetch` 與 async hook。
199
+ - 測試用的 bridge host 會照 App 的規則拒絕:未宣告的權限、`requestOrigins` 以外的網址、非 HTTPS;
200
+ 密碼類 API 只在 Host 啟動的登入流程可用,因此測試中一律拒絕。被拒絕或缺少 fixture 都會讓案例失敗。
201
+ - `test/cases.json`:
202
+
203
+ ```json
204
+ [
205
+ { "name": "popular", "hook": "getPopularManga", "args": [1],
206
+ "expect": { "path": "mangas", "minItems": 2, "snapshot": true } },
207
+ { "name": "pages", "hook": "getPageList", "args": [{ "url": "/manga/1/chapter/1" }],
208
+ "expect": { "minItems": 2, "matches": { "0.imageUrl": "https://example.com/img/1.jpg" } } }
209
+ ]
210
+ ```
211
+
212
+ `expect` 可用 `path`(點路徑)、`minItems`、`notEmpty`、`matches`(子集比對)、`snapshot`
213
+ (存在 `test/snapshots/`)。也可設定 `sourceId`、`preferences`、`credential`(只提供帳號)。
214
+ - fixture 格式:`[{ "request": { "method": "GET", "url": "…", "body"?: "…" }, "response": { "status": 200, "headers": {…}, "body": "…" } }]`。
215
+ 錄製時不會保存 `Set-Cookie`/`Authorization`/`Cookie`。
216
+
217
+ > TODO:App 的 `sandbox-core` WASM guest 公開後,`test` 會改用它執行(`src/runner/sandbox.ts` 的引擎介面已照
218
+ > SandboxProtocol v1 設計,CBOR codec 在 `src/core/sandbox-protocol.ts`),讓測試執行與 App 完全相同的程式碼。
219
+
220
+ ### 6. 在 App 上試用:`serve`
221
+
222
+ ```bash
223
+ shinsou-plugin serve # 只在本機 127.0.0.1:8787
224
+ shinsou-plugin serve --host lan # 自動選本機的區域網路 IPv4
225
+ shinsou-plugin serve --host 192.168.1.20 --port 9000
226
+ ```
227
+
228
+ - 提供 `build/repo/` 的**未簽章**開發者儲存庫,檔案一改就自動重建(失敗時繼續提供上一個成功的版本)。
229
+ - 只接受 `127.0.0.1`、`::1` 或**這台電腦**的私有區域網路位址(10/8、172.16/12、192.168/16);拒絕 `0.0.0.0` 與公開 IP。
230
+ - 印出 `shinsou://add-dev-repository?url=…` 連結與終端機 QR code,用手機掃描即可加入。
231
+ - App 已能處理這個連結(`shinsou://add-dev-repository?url=…`):開啟「設定 → 套件開發者模式」且位址是允許的開發伺服器位址時才會加入;開發中的套件只在沙箱執行、標示「開發中」,不能取得額外放寬。
232
+ - `GET /__shinsou-dev/status` 回傳目前的 build revision 與 SHA-256,可用來判斷是否需要重新載入。
233
+
234
+ ### 7. 簽章:`sign`
235
+
236
+ ```bash
237
+ shinsou-plugin sign --key mykey
238
+ shinsou-plugin sign --key mykey --mirror https://you.github.io/plugins --mirror https://cdn.example.com/plugins
239
+ ```
240
+
241
+ - 為 `repo.json`、`index.json`、每個 sidecar 與 `publisher-revocations.json` 產生 `shinsou-signed-envelope-v1`。
242
+ - **簽章綁定儲存庫網址**,所以每個鏡像各簽一份,輸出到 `public/<鏡像網址>/`,例如
243
+ `public/you.github.io_plugins/`。腳本 `plugins/<id>.js` 原樣複製(App 用 index 的 SHA-256 驗證)。
244
+ - `repo.json` 的 `meta.signingKeyFingerprint` 自動填入簽章金鑰的指紋。
245
+ - 每份文件的 `sequence` 記在 `.shinsou/state.json`,**嚴格遞增**:內容沒變就沿用原本的 sequence(新增鏡像或重跑時
246
+ 簽出相同內容),內容一變就 +1。App 會拒絕較小的 sequence(重播)與同 sequence 不同內容。
247
+ **`.shinsou/state.json` 必須提交到版本控制**(不含任何秘密),否則換電腦或 CI 會從 1 重新開始而被 App 拒絕。
248
+ - 撤回清單預設有效 30 天(`--days` 可改);剩不到一半時 `sign` 會自動延長。第一次簽章時,這把金鑰成為
249
+ 發佈者 ID(`publisherId` = 第一把金鑰的指紋,之後輪替也不變)。
250
+
251
+ ### 7.1 簽署既有的多套件儲存庫:`sign-repo`
252
+
253
+ 手工維護、一個儲存庫放多個套件(`repo.json` + `index.json` + `sidecars/` + `plugins/`)時,不需要改成
254
+ `shinsou.plugin.json` 專案,直接在儲存庫根目錄簽署:
255
+
256
+ ```bash
257
+ shinsou-plugin sign-repo --mirror https://raw.githubusercontent.com/<帳號>/<儲存庫>/refs/heads/main --key <金鑰名稱>
258
+ ```
259
+
260
+ - 不會修改任何原始檔;每個套件的腳本必須符合 index 的 `sha256` 與 `byteSize`,Shinsou 契約的套件還要通過和
261
+ `build` 相同的 index/sidecar/腳本一致性檢查,任一不符就整個拒絕簽署。
262
+ - 其他契約的項目(例如 App 會略過的 `shuyue`)只核對 SHA-256,仍隨 index 一起簽署。
263
+ - 輸出在 `signed/<鏡像>/`:簽章後的 `repo.json`、`index.json`、每個 sidecar、`publisher-revocations.json`,以及腳本副本。
264
+ 把它的內容放到該網址的根目錄即可。`.shinsou/state.json` 記錄序號與撤回清單,必須一起提交。
265
+
266
+ ### 8. 發佈
267
+
268
+ 把 `public/<鏡像>/` 的內容原樣放到對應網址(GitHub Pages、raw、任何靜態主機)。使用者加入 `https://…/plugins`
269
+ 這個基底網址;可以發 `shinsou://add-repository?url=<基底網址>&fp=<完整指紋>` 連結讓 App 自動比對指紋。
270
+
271
+ ### 9. 更新版本
272
+
273
+ 修改程式碼 → 把 `versionCode`(與 `version`)遞增 → `build` → `lint` → `test` → `sign`。
274
+
275
+ ### 10. 撤回、續簽、輪替、撤銷金鑰
276
+
277
+ ```bash
278
+ shinsou-plugin revoke 850c81d9…f7a9 # 撤回某個已發佈版本(腳本 SHA-256),可一次多個
279
+ shinsou-plugin refresh # 只重簽撤回清單、延長 expiresAt(CI 用)
280
+ shinsou-plugin rotate --key oldkey --to newkey # 金鑰輪替
281
+ shinsou-plugin revoke-key --key mykey # 金鑰自我撤銷
282
+ ```
283
+
284
+ - **revoke**:把 SHA-256 加入撤回清單並重簽;App 取得後會卸載並停用這些版本。
285
+ - **refresh**:App 在發佈者的撤回清單過期後,不再從該發佈者安裝新套件或更新(已安裝的繼續執行並顯示警示)。
286
+ 所以**必須定期 refresh**。`init` 已附上 `.github/workflows/shinsou-refresh.yml`(每週一次),也可用
287
+ `shinsou-plugin ci-template` 寫入。在 GitHub 設定 secrets:
288
+ - `SHINSOU_PLUGIN_KEY`:金鑰檔 `~/.config/shinsou-plugin/keys/<名稱>.json` 的完整內容
289
+ - `SHINSOU_PLUGIN_PASSPHRASE`:密語(金鑰有加密時)
290
+
291
+ workflow 會提交更新後的 `public/` 與 `.shinsou/state.json`。
292
+ - **rotate**:舊金鑰簽 `publisher-key-rotation.json` 的 envelope,新金鑰簽 payload 裡的 `newKeySignature`
293
+ (`dev.shinsou.publisher.rotation.v1\0` + canonical JSON 聲明),接著用新金鑰重簽全部文件。
294
+ 新金鑰的密語可用 `SHINSOU_PLUGIN_NEW_PASSPHRASE`。`publisher-key-rotation.json` 請長期保留,
295
+ 還沒更新的使用者靠它換 pin。之後舊金鑰不能再簽。
296
+ - **revoke-key**:由被撤銷的金鑰自己簽 `publisher-key-revoked.json`。撤銷目前金鑰後,這個發佈者的套件全部停用,
297
+ 使用者必須重新信任新金鑰;`sign` 也會拒絕繼續使用它。撤銷已退休的金鑰只會讓它不再能驗證任何東西。
298
+
299
+ ## 開發本工具
300
+
301
+ ```bash
302
+ npm install
303
+ ```
304
+
305
+ ```bash
306
+ npm test # 契約向量、CBOR、政策檢查、完整作者流程(node:test + tsx)
307
+ ```
308
+
309
+ ```bash
310
+ npm run typecheck && npm run build # 輸出 dist/cli.js
311
+ ```
312
+
313
+ - `test/fixtures/contract-v1/` 複製自 App 的 `fixtures/plugin-contract/v1/`。兩邊不一致代表協定改變,
314
+ 不是可以隨手更新的快照。
315
+ - `src/runner/legacy-bootstrap.ts` 由 `node scripts/sync-bootstrap.mjs [shinsoux 路徑]` 從 App 的
316
+ `IosJavaScriptBootstrap.kt` 產生,App 的 bootstrap 改變時要重跑。
317
+
318
+ | 模組 | 內容 |
319
+ |---|---|
320
+ | `src/core/canonical-json.ts` | 與 App `canonicalSyncJson` 相同的正規化 JSON(UTF-16 鍵排序、只接受整數) |
321
+ | `src/core/envelope.ts` | envelope 簽章與驗證(錯誤分類與 App 相同:`Malformed`/`KeySubstitution`/`InvalidSignature`) |
322
+ | `src/core/publisher.ts` | 輪替聲明、撤回清單、金鑰自我撤銷 |
323
+ | `src/core/sandbox-protocol.ts` | SandboxProtocol v1 嚴格 CBOR codec、CHUNK 分段與串流讀取 |
324
+ | `src/core/manifest.ts`/`build.ts`/`consistency.ts`/`lint.ts` | 單一事實來源、打包、三方一致檢查、政策檢查 |
325
+ | `src/core/keystore.ts`/`state.ts`/`publish.ts` | 金鑰、sequence 狀態、簽章流程 |
326
+ | `src/runner/*` | 沙箱引擎介面、QuickJS 實作、fixture bridge host、測試 harness |
327
+
328
+ ## 已知限制
329
+
330
+ - `contentHostSuffixes` 與 `COOKIE_READ` 已由 App 支援;內建的 Public Suffix List 是固定版本,與 App 版本不同步時
331
+ (App 更新清單後),某個後綴可能在新版變成公開後綴而被 App 拒絕安裝;更新時請同步替換 `data/public_suffix_list.dat`。
332
+ - 一個專案只產生一個套件;同一發佈者的多個套件目前要分開的儲存庫(或自行合併 index)。
333
+
334
+ ## 授權
335
+
336
+ [MIT](LICENSE)
package/data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # Public Suffix List
2
+
3
+ `public_suffix_list.dat` 是 [Public Suffix List](https://publicsuffix.org/)(MPL 2.0)的副本,
4
+ 與 Shinsou X App 內建、固定版本的清單完全相同(App 的 `composeApp/psl/public_suffix_list.dat`)。
5
+ `lint` 用它以和 App 相同的規則檢查 `contentHostSuffixes`(必須是可註冊網域)。
6
+
7
+ - 來源:publicsuffix/list `main` @ `94ec5df7273926560d91e77c002000ba1a53fec4`(2026-11-08 取得)
8
+ - SHA-256:`762b6cbdd2dacc597b398fcd7860e2184b684125a58aa177dbdf54916a1f3430`
9
+
10
+ 更新時請與 App 同步換成同一份檔案,並執行 `npm test`(測試會比對上面的 SHA-256)。