開放的套件生態:沒有官方套件,只有你選擇信任的發佈者。

Shinsou X 以 JavaScript 套件擴充漫畫與小說來源。每個套件都由作者自己的金鑰簽章、在沙箱中執行,只取得你逐項同意的權限。任何人都可以寫套件並自行發佈。

Principles

每個套件都是第三方套件

Shinsou X 不內建、不審核、也不替任何套件背書,包含專案作者自己維護的套件。App 內沒有任何依套件名稱、網址或雜湊值例外放行的清單;安全性來自沙箱隔離、最小權限與使用者逐項授權,而不是來自誰發佈。

發佈者簽章

作者以自己的 Ed25519 金鑰簽署整個儲存庫。你信任的是發佈者金鑰,不是網址。

一律在沙箱執行

腳本在隔離的 QuickJS 沙箱中執行,沒有網路、檔案或子程序權限,只能透過受控的 bridge 向 App 請求。

逐項授權

執行與連線到宣告的網站隨安裝授予;Cookie、帳密、登入、收藏修改等敏感權限預設關閉,由你逐項開啟,也可隨時撤銷。

沒有中央封鎖清單

Shinsou X 不維護任何遠端封鎖清單。唯一能遠端停用套件的,是發佈者自己簽署的撤回清單與金鑰撤銷。

簽章不等於安全

簽章只證明內容出自持有該金鑰的作者;SHA-256 與 sidecar 核對只證明下載的檔案與作者宣告一致。兩者都不代表程式行為安全,也不代表任何人審核過它。只安裝你信任的發佈者。

名稱不能暗示官方背書

名稱含「官方」、「公式」或 official 的套件、來源與發佈者,會在安裝與信任時被拒絕;「非官方」、Unofficial 這類明確的免責聲明則可以使用。

How it works

從儲存庫到閱讀器

儲存庫只是一組靜態檔案,不是內容託管服務。App 負責驗證、授權、網路、敏感資料與閱讀;套件只負責和來源網站溝通,回傳作品、章節與內容。

驗證發佈者

檢查儲存庫簽章、發佈者金鑰與撤回清單是否有效且未過期。

核對宣告

索引、sidecar 與腳本三處的版本、來源、內容種類、網站與權限必須一致;超出沙箱上限的宣告直接拒絕。

驗證腳本

下載後比對 byte size 與 SHA-256,不符就不安裝。

你決定授權

在權限頁看到它會連到哪些網站、要求哪些敏感權限,逐項決定是否開啟。

沙箱執行

來源出現在「瀏覽 → 來源」,書庫、閱讀器與下載都透過 App 受控的介面取得內容。

Shinsou X 擴充套件頁面
在 App 內管理儲存庫、可用套件、已安裝版本與更新狀態。

Repositories

加入儲存庫並確認發佈者

任何發佈者都可以提供自己的儲存庫。加入時 App 會顯示發佈者自稱的名稱(標示為未驗證)與金鑰指紋,請和發佈者在自己網站公布的完整指紋比對。

新增儲存庫

開啟「瀏覽 → 擴充套件 → 新增」,貼上發佈者提供的儲存庫基底網址;或點擊發佈者提供的 shinsou:// 加入連結。

確認發佈者

預設為首次使用即信任;若連結或你自己輸入了完整指紋,就一定要相符才能信任。

選擇套件並授權

重新整理後選擇套件,在權限頁決定要開啟哪些敏感權限。

嚴格模式:一律要求比對指紋

在「套件安全」開啟後,沒有完整指紋或指紋不符就不能信任發佈者。只用首次使用即信任的發佈者會顯示為待比對:已安裝的套件繼續執行,但比對完成前不安裝新套件、也不更新。

社群儲存庫範例

專案作者以一般發佈者身分維護 Shinsou Community Plugins,地位與其他第三方發佈者相同,同樣需要你確認發佈者與逐項授權。需要 App v1.0.1-beta.15 以上。

儲存庫網址:https://raw.githubusercontent.com/aluo96078/shinsou_plugin/refs/heads/master

完整指紋:sha256:o-kWmdLK5JsPBIkzNB0FNsomhKpPqWB5QunWv2O137o

索引格式App 行為
簽章的 Shinsou 儲存庫驗證後可安裝,套件在沙箱中執行
未簽章的儲存庫只在開發者模式、且位址為本機或你輸入的區網位址時載入
Mihon/Tachiyomi 索引只讀取 metadata,APK 不會在 Shinsou X 執行
ShuYue 格式套件不再執行,儲存庫中的這類項目會被略過

Permissions

宣告要求,不等於取得權限

有效權限 = 套件宣告(索引、sidecar、腳本三方一致)∩ App 支援 ∩ 沙箱上限 ∩ 你對這個確切版本的授權。沙箱上限是 App 常數,套件與使用者都無法提高。

EXECUTE_SCRIPT

隨安裝授予:在沙箱中執行套件腳本。

NETWORK

隨安裝授予:只能連到套件宣告的 HTTPS 網站。

COOKIE_STORAGE

敏感:寫入由 App 代管的 Cookie。

COOKIE_READ

敏感:讓腳本讀取 Cookie 的值。沒有此權限時,App 仍會在請求中帶上 Cookie,只是腳本看不到。

CREDENTIAL_ACCESS

敏感:存取已保存的帳號;密碼只在 App 啟動的一次性登入沙箱中出現。

LOGIN_PROMPT

敏感:要求 App 顯示登入畫面。

FAVORITE_MUTATION

敏感:加入或移出網站帳號的收藏。

BROWSER_CHALLENGE

敏感:對宣告的 browserSessionOrigins 使用 Web challenge 與瀏覽器傳輸。

BROWSER_IMAGES

敏感:在 macOS 與 iOS 以一次性私密瀏覽器下載指定 CDN 的圖片,不帶 Cookie 與 Referer。

contentHostSuffixes

敏感:每個 CDN 後綴家族各自一個開關,只放寬不帶憑證的內容下載。

REQUEST_* / REPORT_DIAGNOSTIC

系統事件:要求 App 顯示登入、重新整理來源、登出或寫入診斷記錄,需另外授權。

REPORT_USER_MESSAGE

永不授予:宣告此權限的套件會在安裝前被拒絕。

網路只允許確切的 HTTPS origin

API 請求、登入、內容下載與瀏覽器傳輸各有自己的 origin 清單。App 會釘選 DNS 解析結果、逐跳檢查 redirect 並拒絕降級為 HTTP;圖片與正文由 App 代為下載,不帶任何來源憑證。

Updates & revocation

更新、撤回與金鑰事件

沒有新要求的更新會沿用授權

同一發佈者金鑰、沒有新權限、新 CDN 後綴、新來源或新網站時,更新會自動沿用你先前的決定並通知你;你拒絕過的仍然拒絕。

有新要求就先審核

只要多要求任何東西,或換了發佈者金鑰,你必須在安裝前審核;接受之前舊版本繼續執行。

降版一律拒絕

versionCode 或 digest 回退的版本不會被安裝。

機制App 行為
版本撤回發佈者簽署的撤回清單列出的版本會立即停止執行,並提示更新
撤回清單過期不再從該發佈者安裝或更新;已安裝的繼續執行並顯示警示
金鑰輪替新舊金鑰雙簽,App 自動換成新金鑰,不需重新信任
金鑰自我撤銷該發佈者的套件全部停止執行,需重新信任新金鑰
本機封鎖你可以封鎖單一套件、單一版本或整個發佈者;只存在這台裝置

Sandbox

所有平台都在沙箱中執行

平台沒有可用的沙箱時一律拒絕執行,不會退回 App 內的引擎。每個套件有自己的記憶體、時間與 bridge 呼叫上限,超限以穩定錯誤碼失敗,不回傳部分結果。

平台沙箱Web challenge
Android每個套件一個 isolated process支援時使用隔離的 App 內 WebView,否則開外部瀏覽器
iOS/iPadOSWebAssembly 沙箱App 內 non-persistent WKWebView
macOSdeny-all 的獨立 helper 程序獨立的 non-persistent WKWebView 視窗
Windows低權限 AppContainer helper 程序只開外部瀏覽器,無法自動帶回 Cookie

Content contract

漫畫、小說,也可以在同一個套件

contentKinds內容腳本實作
IMAGE_SEQUENCE漫畫、逐頁圖片getPageList(chapter)
PLAIN_TEXT小說正文,單章最多 4 MiBgetChapterText(chapter)

一個套件可以混合多個來源,每個來源各自宣告內容種類;App 依確切的 sourceId 選擇來源,不依陣列順序。sourceId 一經發佈就不能更改,書庫、閱讀進度與登入都以它綁定。

Data boundary

備份帶走儲存庫清單,不帶走套件與秘密

資料備份與還原
儲存庫清單與書庫、閱讀進度可攜,依備份選項納入可攜備份
已安裝的套件、信任與授權只在這台裝置;新裝置需重新安裝並授權
Cookie、密碼、token、API key不進任何備份

For developers

開發指引:從金鑰到使用者安裝

套件是一支 JavaScript 腳本加上一份宣告檔。儲存庫是靜態檔案,可以放在 GitHub Pages、raw GitHub 或任何靜態主機,不需要伺服器,也不需要任何人核准上架。

keygen → init → build → lint → test → serve (developer mode) → sign → publish
then:  bump versionCode → build / lint / test → sign
       refresh (scheduled) / revoke / rotate / revoke-key

1. 安裝 CLI

需要 Node.js 20 以上。CLI 尚未發佈到 npm,請從 GitHub 安裝:

npm install --global github:aluo96078/shinsou-plugin-cli
shinsou-plugin help

2. 產生發佈者金鑰

shinsou-plugin keygen --name mykey --passphrase
shinsou-plugin fingerprint --key mykey
私鑰就是你的身分

加密並備份金鑰檔,不要提交到版本控制。遺失私鑰無法恢復,使用者只能改信任新金鑰並重新授權。請在你的網站與 README 公布完整指紋(sha256: 開頭),比對只認完整值。

3. 建立專案

shinsou-plugin init my-plugin --template manga --id zh.example --name "Example" --lang zh
範本用途
manga解析 HTML 的漫畫來源
novel小說來源,實作 getChapterText
mixed同一套件內的漫畫與小說兩個來源
asyncJSON API、非同步 hook 與多檔模組

4. 撰寫宣告檔

shinsou.plugin.json 是唯一的事實來源,索引、sidecar 與腳本內的宣告都由它產生。只宣告真正用到的能力、權限與網站。

{
  "manifestVersion": 1,
  "packageId": "zh.example",
  "name": "Example",
  "version": "1.0.0",
  "versionCode": 1,
  "lang": "zh",
  "contract": "shinsou",
  "runtime": "legacy-shinsou-adapter-v2",
  "contentType": "manga",
  "contentKinds": ["IMAGE_SEQUENCE"],
  "capabilities": ["CATALOGUE", "LATEST", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
  "runtimePermissions": ["EXECUTE_SCRIPT", "NETWORK"],
  "requestedHostPermissions": [],
  "systemEvents": { "required": [], "optional": [] },
  "entry": "src/index.js",
  "sources": [{
    "sourceId": "1234567890123456789",
    "name": "Example",
    "lang": "zh",
    "baseUrl": "https://example.com",
    "requestOrigins": ["https://example.com"],
    "credentialOrigins": [],
    "contentOrigins": ["https://img.example.com"],
    "browserSessionOrigins": [],
    "contentHostSuffixes": []
  }],
  "repository": {
    "name": "My Plugins",
    "website": "https://github.com/you/plugins",
    "mirrors": ["https://you.github.io/plugins"]
  }
}

packageId

小寫英數與點、底線、連字號,不可含 ~,最長 111 字元。不同發佈者可以使用相同 packageId,App 會視為不同套件並存安裝。

versionCode

每次發佈都必須遞增,App 拒絕降版。

sourceId

64-bit 整數字串;或小寫點分隔字串搭配數字 legacyLongId。發佈後不可更改。

origin 欄位

全部是確切的 HTTPS origin,不含路徑;不接受 HTTP、IP、localhost 與私有網路主機。

Script API

腳本結構與可呼叫的 bridge

腳本是 classic script,進入點在頂層宣告 source 物件(多來源用 sources 陣列);build 會把 import 打包成單一檔案。Hook 可以回傳 Promise。

var source = {
  baseUrl: "https://example.com",
  supportsLatest: true,

  getPopularManga: function (page) {
    var doc = Jsoup.parse(bridge.httpGet(this.baseUrl + "/popular?page=" + (page + 1)), this.baseUrl);
    var list = doc.select(".item").map(function (el) {
      var m = SManga.create();
      m.url = el.selectFirst("a").attr("href");
      m.title = el.selectFirst(".title").text();
      m.thumbnail_url = el.selectFirst("img").absUrl("src");
      return m;
    });
    return new MangasPage(list, list.length > 0);
  },

  getChapterList: function (manga) { /* return SChapter[] */ },
  getPageList: function (chapter) { /* return [new Page(0, "", imageUrl), ...] */ }
};
Hook用途
getPopularManga / getLatestUpdates / getSearchManga回傳作品清單與是否有下一頁
getMangaDetails / getChapterList補完作品資料、取得章節
getPageList / getChapterText漫畫圖片頁/小說章節文字
getFavoriteManga / setFavorite選用:網站帳號的書架與收藏
login(username, password)選用:在一次性沙箱中執行的登入流程
Bridge需要的權限
bridge.httpGet / httpPost / await bridge.net.fetchNETWORK,只限 requestOrigins
bridge.setCookie / deleteCookieCOOKIE_STORAGE
bridge.getCookie / getCookiesCOOKIE_STORAGE + COOKIE_READ
bridge.getCredentialUsername / hasCredentialCREDENTIAL_ACCESS
bridge.requestLoginLOGIN_PROMPT
bridge.browserSessionRequestBROWSER_CHALLENGE
bridge.getPreference / setPreference無
為被拒絕的權限設計降級

使用者可以不開啟敏感權限。缺少權限的呼叫會以 PERMISSION_DENIED 失敗,腳本應該在這種情況下仍能提供基本功能。

Build、lint 與 test

shinsou-plugin build          # bundle, digest, index + sidecar; verifies all three agree
shinsou-plugin lint           # undeclared bridge calls, non-HTTPS origins, misleading names
shinsou-plugin test           # replay recorded HTTP fixtures in a QuickJS sandbox
shinsou-plugin test --record  # record missing fixtures from the live site

在 App 上試用:開發者模式

在 App 的「套件安全」開啟開發者模式,就能載入未簽章的開發儲存庫。手機測試時,在開發伺服器位址填入電腦的私有區網 IPv4 位址。

shinsou-plugin serve              # http://127.0.0.1:8787
shinsou-plugin serve --host lan   # private LAN IPv4, prints a shinsou:// link and QR code

不會取代正式安裝

開發中的套件以獨立身分安裝並標示「開發中」,只在開發者模式開啟時執行。

沒有額外放寬

開發儲存庫可以用 HTTP,但套件自己的請求仍只能是 HTTPS,敏感權限一樣要逐項開啟。

Sign & publish

簽章並發佈到靜態主機

shinsou-plugin sign --key mykey --mirror https://you.github.io/plugins

每個網址各簽一份

簽章綁定儲存庫網址,每個鏡像輸出到各自的目錄。上傳後的網址必須和簽章時完全一致。

提交 .shinsou/state.json

它記錄每份文件嚴格遞增的序號,不含秘密。遺失它會讓新簽的文件被 App 當成重播而拒絕。

公布加入方式

在你的網站提供儲存庫網址、完整指紋與 shinsou:// 加入連結,讓使用者能獨立核對。

repo.json
index.json
sidecars/<packageId>.json
plugins/<packageId>.js
publisher-revocations.json
publisher-key-rotation.json   (after rotate)
publisher-key-revoked.json    (after revoke-key)

Maintenance

維護你的發佈

定期續簽撤回清單(必做)

撤回清單預設 30 天到期。過期後使用者無法再安裝或更新你的套件,即使你沒有任何撤回也一樣。init 已附上每週執行的 GitHub Actions workflow,請在 secrets 設定 SHINSOU_PLUGIN_KEY 與 SHINSOU_PLUGIN_PASSPHRASE。

shinsou-plugin refresh --key mykey                  # renew the revocation list
shinsou-plugin revoke <sha256> --key mykey           # pull a bad version
shinsou-plugin rotate --key mykey --to newkey        # rotate keys, users keep trust
shinsou-plugin revoke-key --key mykey                # leaked key: disable everything it signed

小修補不要加權限

沒有新要求的更新會自動沿用授權;新增權限或網站會讓使用者在更新前重新審核。

長期保留輪替檔

還沒更新的使用者靠 publisher-key-rotation.json 換成新金鑰。

在各平台實測

離線測試不會驗證第三方登入、Cloudflare、限流或網站 API 變更,請用開發者模式在真實 App 試過。

GitHub Actions

用 GitHub Actions 自動簽章

把金鑰放進儲存庫的 secrets 後,GitHub Actions 可以在你推送程式碼時自動檢查、簽章並發佈,也能定期續簽撤回清單,不必每次在自己電腦上手動執行。

Workflow觸發時機做什麼
shinsou-refresh.ymlinit 內建;每週一次,或手動觸發只續簽撤回清單,並提交簽章檔與序號
shinsou-sign.yml自行加入;推送原始碼時、每週一次,或手動觸發lint、test、簽章、到期前續簽撤回清單,再部署到 GitHub Pages

1. 把 CLI 依賴固定到 GitHub 版本

init 產生的 package.json 依賴的 CLI 版本尚未發佈到 npm,Actions 執行 npm ci 時會失敗。請改成固定的 GitHub commit:

npm install --save-dev github:aluo96078/shinsou-plugin-cli#<commit>

2. 設定 secrets

在 GitHub 儲存庫的 Settings → Secrets and variables → Actions 新增兩個 secret,或使用 GitHub CLI:

gh secret set SHINSOU_PLUGIN_KEY < ~/.config/shinsou-plugin/keys/mykey.json
gh secret set SHINSOU_PLUGIN_PASSPHRASE

SHINSOU_PLUGIN_KEY

金鑰檔的完整 JSON 內容。CLI 會依其中的名稱找到這把金鑰。

SHINSOU_PLUGIN_PASSPHRASE

金鑰的密語;只有金鑰有加密時才需要,但強烈建議加密。

3. 推送時自動簽章並部署

以下範例在原始碼改變時檢查並簽章、每週在撤回清單剩不到一半效期時續簽,最後把簽好的鏡像目錄部署到 GitHub Pages。請在 Settings → Pages 把來源設為 GitHub Actions,並把 SITE_DIR 換成 sign 輸出的鏡像目錄。

# .github/workflows/shinsou-sign.yml
name: Sign and publish

on:
  push:
    branches: [main]
    paths: ["src/**", "test/**", "shinsou.plugin.json"]
  schedule:
    - cron: "17 3 * * 1" # every Monday 03:17 UTC
  workflow_dispatch:

permissions:
  contents: write
  pages: write
  id-token: write

concurrency:
  group: shinsou-sign
  cancel-in-progress: false

env:
  SITE_DIR: public/you.github.io_plugins # the mirror directory written by sign

jobs:
  sign:
    runs-on: ubuntu-latest
    environment: github-pages
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx shinsou-plugin lint
      - run: npx shinsou-plugin test

      - name: Sign
        if: github.event_name != 'schedule'
        env:
          SHINSOU_PLUGIN_KEY: ${{ secrets.SHINSOU_PLUGIN_KEY }}
          SHINSOU_PLUGIN_PASSPHRASE: ${{ secrets.SHINSOU_PLUGIN_PASSPHRASE }}
        run: npx shinsou-plugin sign --key "$(node -p 'JSON.parse(process.env.SHINSOU_PLUGIN_KEY).name')"

      - name: Renew the revocation list when due
        env:
          SHINSOU_PLUGIN_KEY: ${{ secrets.SHINSOU_PLUGIN_KEY }}
          SHINSOU_PLUGIN_PASSPHRASE: ${{ secrets.SHINSOU_PLUGIN_PASSPHRASE }}
        run: npx shinsou-plugin refresh --if-due --key "$(node -p 'JSON.parse(process.env.SHINSOU_PLUGIN_KEY).name')"

      - name: Commit signatures and sequence numbers
        run: |
          git config user.name "shinsou-sign"
          git config user.email "[email protected]"
          git add .shinsou/state.json public
          git diff --cached --quiet || { git commit -m "chore: sign repository"; git push; }

      - uses: actions/upload-pages-artifact@v3
        with:
          path: ${{ env.SITE_DIR }}
      - uses: actions/deploy-pages@v4

sign 會自己 build

sign 會先重新打包,再依 shinsou.plugin.json 的 repository.mirrors 為每個鏡像各簽一份。

一定要提交序號檔

workflow 會把 .shinsou/state.json 推回儲存庫;少了它,下一次簽章的序號會倒退而被 App 拒絕。

只保留一個 workflow

使用這個範例後,可以刪除 init 附的 shinsou-refresh.yml。兩者共用同一個 concurrency 群組,不會同時寫入序號檔。

先手動觸發一次

在 Actions 頁選擇這個 workflow 按 Run workflow,確認出現簽章 commit、Pages 部署成功,再用 App 重新整理儲存庫。

保護 CI 裡的私鑰

只把金鑰 secrets 傳給需要簽章的步驟,不要設在整個 job,避免 npm 套件的安裝腳本讀到。不要讓 pull_request_target 或 fork 的程式碼觸發簽章;限制誰能推送到主分支。金鑰外洩時立刻執行 revoke-key 並更換 secrets。

AI Skill

讓 AI 開發助手協助撰寫套件

shinsou-extension-creator 是給 AI 開發助手使用的工作指南,協助建立、遷移或修復套件:保持穩定的來源身分、如實宣告內容、最小化權限,並讓腳本、sidecar 與索引一致。它不是 App 內的套件,也不會替你簽章或發佈。

AI 產生的程式碼仍由你負責

發佈前請自己執行 build、lint 與 test,並在開發者模式中實測。不要把私鑰或密語貼給 AI 助手。

FAQ

套件常見問題

Shinsou X 有官方套件嗎?

沒有。每個套件都是第三方套件,包含專案作者自己維護的。App 不替任何發佈者背書,請依你對發佈者的信任決定是否安裝。

為什麼加入社群儲存庫後讀不到套件?

社群儲存庫使用字串 sourceId,需要 App v1.0.1-beta.15 以上。也請確認網址是儲存庫基底網址,而不是 GitHub 專案頁面。

為什麼不能安裝或更新某個發佈者的套件?

最常見的原因是該發佈者的撤回清單過期或缺少。已安裝的套件會繼續執行並顯示警示,需要等發佈者續簽。

Shinsou X 能安裝 Mihon APK 擴充嗎?

不能。Mihon 索引只作為 metadata 相容輸入,APK 不會在 Shinsou X 執行。

兩個發佈者都發佈了相同 packageId 怎麼辦?

App 把它們視為不同套件,可以並存安裝;信任、授權與書庫綁定分開計算。

為什麼另一台裝置有儲存庫,卻沒有來源或登入狀態?

儲存庫清單包含在可攜備份中,但已安裝的套件、授權、Cookie 與帳密都不在備份內,只存在原本的裝置。請在新裝置重新安裝、授權並登入。

網站可以開啟,來源卻載入失敗怎麼辦?

依序檢查登入、Cookie、User-Agent、Web challenge 與來源的 proxy 設定。Windows 的外部瀏覽器完成驗證後不會自動把 Cookie 帶回 App,可改用 Cookie 匯入。