🌐 English · English version
Life is short, connect buddies.
去中心化、無中央資料庫的即時通訊軟體。融合 Nostr 協議 與 WebRTC,高度還原早期 MSN Messenger 的經典互動體驗(震動、音樂狀態、離線留言),並以「零伺服器維護成本」為營運目標。
🔥 官方網站:https://vaalrl.github.io/Cinderous/ | 🌐 在瀏覽器開啟(網頁版):https://cinderous.cinderous1.workers.dev | 💻 下載桌面版:GitHub Releases
Cinderous 透過「狀態信令」與「巨量資料傳輸」雙軌混合網路,達成極致隱私與低延遲:
- 零知識身分:首次啟動於本機生成 secp256k1 金鑰對(Nostr/NIP-01),公鑰(
npub)即為全網唯一 ID,無帳號密碼。同一裝置可並存多個身分(工作/個人)。 - 本機優先・靜態加密:對話與私鑰留在裝置;localStorage/OPFS 上的資料以裝置金鑰(由 nsec 導出)AES-256-GCM 加密(ADR-0112),Tauri 桌面另把私鑰託給 OS 金鑰庫。明文永不上雲。
- 端到端加密:訊息以 Nostr NIP-17/44/59 Gift Wrap 加密後才離開裝置——中繼站看不到內容,寄件者也以一次性金鑰隱藏(訊息層社交圖譜不可見;在線訂閱仍會讓 relay 得知你的聯絡人集合,見
docs/SECURITY.md殘餘)。 - 換裝置有路、預設純本機:預設純本機(裝置全毀=身分終止);也提供選擇性的還原路徑——本地密碼記住(Argon2id)、裝置配對搬家(P2P 全程加密)、加密雲端備份與備份碼(NIP-49),全程仍是密文,明文/私鑰不外流。
雙軌混合網路(依連線狀態動態切換):
| 引擎 | 技術 | 定位 |
|---|---|---|
| 引擎 A:狀態與信令 | Nostr 協議(Cloudflare Workers;SQLite-backed Durable Objects) | 離線留言暫存、線上狀態廣播、WebRTC 初始 SDP 信令交換 |
| 引擎 B:巨量資料傳輸 | WebRTC 直連(P2P) | 即時互動:震動(Nudge)、動畫快遞、大檔案傳輸,繞過中繼站達成毫秒級延遲 |
資料生命週期靠 NIP-40(7 天過期)與 Ephemeral Events(Kind 20000-29999 純記憶體轉發、不寫資料庫),確保 Cloudflare 永久免費額度不被耗盡。
- 桌面端(第一優先):Rust Tauri + React/TypeScript,可打包成 Windows(MSI/NSIS)/macOS/Linux 安裝檔;私鑰託 OS 金鑰庫、背景 WebSocket 長連線、原生檔案存取(公司儲存槽等)。同一套 React UI 也能直接跑在瀏覽器(開發與 web 執行環境),以加密 localStorage/OPFS 落地——並非捨棄網頁,而是把「明文絕不落盤」做到瀏覽器(ADR-0112)。
- 行動端(規劃中):目前為 React Native Web(與桌面共用同一套
RelayChatBackendengine、功能高度對齊);Android 已有測試版 APK(見 Releases,Capacitor 殼,自 v0.0.14 起);🔴 它目前以 debug 憑證簽署且可被偵錯——能接上 USB 偵錯的人可讀取行程記憶體,而 session 期間私鑰必然在裡面,請勿用它處理你真正在意的對話(ADR-0335,詳見docs/SECURITY.md)。iOS 尚未推出(另需 macOS 建置)。原生通知與背景推播待接。 - 中繼站:Cloudflare Worker(Durable Objects 扇出),另有 Node 版可自架於容器。
完整技術規格見 PRD.md,模組邊界與資料流見 ARCHITECTURE.md。
.
├── apps/
│ ├── desktop/ # Tauri + React + TypeScript(第一優先平台)
│ │ ├── src/ # React/TS 前端 UI 與瀏覽器 demo(demo.html / webrtc.html)
│ │ └── src-tauri/ # Rust:SQLite、Nostr WebSocket、WebRTC、金鑰管理
│ └── mobile/ # React Native + SQLite(輔助平台,預留)
├── packages/
│ └── core/ # 共用 TS:Nostr 事件、secp256k1 簽章、NIP-44/17/59、型別、Kind 常數
├── relay/ # Cloudflare Worker(Nostr 中繼站;Durable Objects 扇出,離線留言預留 DO SQLite)
└── docs/adr/ # 架構決策紀錄(ADR)
從核心協定到桌面/行動客戶端與企業模式皆已實作並通過測試(pnpm monorepo、TypeScript strict、全程 TDD)。摘要如下(詳見 docs/adr/ 逾 330 份決策紀錄):
核心通訊
- Nostr 中繼:心跳/上線狀態、Ephemeral 扇出、離線留言(NIP-17/44/59 Gift Wrap + NIP-40 過期)
- WebRTC:真實 P2P(
RTCPeerConnection/ICE/TURN)語音/視訊通話、資料通道分塊檔案傳輸、雙軌降級與自動改道 - 多裝置:QR/配對搬家(P2P,AES-256-GCM)、加密雲端備份(opt-in)、備份碼(NIP-49)、多身分並存;我的裝置清單+移除裝置(ADR-0322:簽章裝置目錄、加密子鑰逐台分發)——⚠ 那買到的是裝置遺失/被偷/舊機沒清,不是身分私鑰外洩的解藥,設定頁如實說明並顯示裝置金鑰的保護等級(ADR-0323)
即時通體驗(懷舊 MSN 風)
- 敲一下(Nudge/震動)、正在輸入、正在聽(音樂狀態)、自訂狀態文字、隱身、上線狀態本機還原
- 表情回應、收回訊息、已讀回條、@提及、內嵌回覆/對話串、限時訊息(閱後即焚)
- 群組(建立/成員管理)、貼圖(內建+自製)、對話背景、私人便條(加密)、媒體相簿
- 頭像廣播、本地暱稱/標籤、依聯絡人音效、訊息請求/封鎖/移除聯絡人
企業模式(公司帳號)
-
組織名冊(管理者簽章)、邀請碼入職、公司帳號金鑰託管+離職接管
-
公司設定(歡迎詞/上下班時間、下班自動靜音)、訊息保留天數、公司儲存槽(檔案存放)
-
企業成員鎖公司座;桌面與行動端皆可管理(唯「企業主收儲存槽落盤」限桌面=需原生檔案系統)
-
測試:core 639/engine 570/desktop 714/mobile 441/relay 197/website 68/theme 37/cli 30/i18n 16/brand 4(TS 共 2,716)+ Rust,含 relay↔client 端到端整合測試與行動端 jsdom 互動測試(ADR-0328)。
-
可眼見:桌面客戶端可打包成安裝檔(見上「終端平台」),亦可直接在瀏覽器跑(
/);另有端到端demo.html/真實 WebRTCwebrtc.html。 -
完整施工計畫(現況、各平台待辦、M6–M9 進階功能、相依順序):見
docs/ROADMAP.md。 -
使用者教學(給一般使用者:身分與鑰匙、加好友、本地密碼、換裝置三條路、搬家、常見問題):見
docs/使用手冊_User-Guide.md。 -
前端開發指南(給社群開發者:三層架構、重用 core/i18n、消費
ChatBackend接自己的 UI、AGPL):見docs/前端開發指南_Frontend-Guide.md(ADR-0074)。
需求:Node 22+、pnpm 10+(Rust 測試另需 stable toolchain)。
# 1. 取得原始碼並安裝 workspace 相依
git clone https://github.com/VaalRL/Cinderous.git && cd Cinderous
pnpm install
# 2. 驗證環境(全綠代表就緒)
pnpm -r test # 全部 TS 測試(core / engine / i18n / relay / desktop / mobile)
pnpm -r typecheck # 全部型別檢查
# 3. 起本機真實 relay,再跑桌面前端(瀏覽器開發、不需 Tauri)
pnpm --filter @cinderous/relay build:dev && pnpm --filter @cinderous/relay dev # ws://localhost:8787
pnpm --filter @cinderous/desktop dev # 前端;開 /?relay=ws://localhost:8787想接自己的前端、加語言、做擴充或貢獻核心? 開發者入口見
.github/CONTRIBUTING.md(三層架構、環境、參與方式、規範),接前端的完整教學見docs/前端開發指南_Frontend-Guide.md。
| 任務 | 指令 |
|---|---|
| 桌面前端(懷舊即時通風格) | pnpm --filter @cinderous/desktop dev,開啟 /(另有端到端 demo /demo.html、真實 WebRTC /webrtc.html) |
| 前端建置 | pnpm --filter @cinderous/desktop build |
| 共用核心測試 | pnpm --filter @cinderous/core test |
| 中繼站測試 | pnpm --filter @cinderous/relay test |
| 本機真實 relay(開發用) | pnpm --filter @cinderous/relay build:dev && pnpm --filter @cinderous/relay dev(起 ws://localhost:8787;前端開 /?relay=ws://localhost:8787 即連真實 relay) |
| Rust 測試 | cargo test(於 apps/desktop/src-tauri/) |
| 桌面端開發(需 Tauri 工具鏈) | pnpm --filter @cinderous/desktop tauri dev |
| 中繼站本地開發 / 部署(需 wrangler) | wrangler dev / wrangler deploy(於 relay/) |
中繼站(relay/)是一個自建的最小 Nostr relay,跑在 Cloudflare Workers 上,以
Durable Object 持有所有 WebSocket 連線並做記憶體扇出。它的職責是:
- 線上狀態廣播(心跳 Kind 20000)、正在輸入中(20001)、音樂狀態(20002);
- WebRTC 的 SDP / ICE 信令交換(NIP-59 包封的 ephemeral 事件,Kind 21000-21999);
- 離線留言暫存(NIP-17/59 Gift Wrap + NIP-40 過期;
message-store邏輯已實作並測試——每收件人配額、以#p為索引。持久化:Cloudflare 版用 SQLite-backed Durable Object(new_sqlite_classes)、Node 自架版用內建 SQL)。
WebRTC 一旦 P2P 打通,震動、檔案傳輸等資料完全走點對點,永遠不經過 relay。 Ephemeral 事件只在記憶體轉發、不落地;relay 看不到訊息明文,也看不到(經 Gift Wrap 隱藏的)社交圖譜。
- 一個 Cloudflare 帳號
- Node 22+、pnpm 10+
- 注意:本 relay 使用 Durable Objects。Durable Objects 目前需要 Workers 付費方案(約 US$5/月) 或符合 Cloudflare 當前免費方案條件——部署前請以 官方計費說明 為準。
# 1. 取得原始碼並安裝相依(會建立 workspace 連結,wrangler 打包時需要)
git clone <your-fork-url> cinder && cd cinder
pnpm install
# 2. 登入 Cloudflare(會開瀏覽器授權)
cd relay
npx --yes wrangler@4 login
# 3. 本地開發:在 ws://127.0.0.1:8787 起一個本機 relay
npx --yes wrangler@4 dev
# 4. 部署到 Cloudflare,取得 wss://cinder-relay.<你的帳號>.workers.dev
pnpm run deploy⚠ 用
npx而不是pnpm dlx:pnpm 10 的建置腳本需明示同意,pnpm dlx wrangler會以ERR_PNPM_IGNORED_BUILDS中止(--allow-build在 dlx 下無效)。
部署設定在 relay/wrangler.toml:Worker 名稱、進入點 src/worker.ts、
以及 Durable Object 綁定(RELAY_ROOM → RelayRoom 類別與 migration)。wrangler 會直接打包
TypeScript,無需額外建置步驟。可在 wrangler.toml 修改 name 換成你自己的 Worker 名稱。
🔴 推上 GitHub 不會部署中繼站。 這個專案沒有任何自動部署流程——.github/workflows/
裡沒有 wrangler、也沒有 Cloudflare 憑證。中繼站永遠是由人在自己的機器上跑 pnpm run deploy
推上去的。
這表示兩件事,自架者要知道:
- 未提交的本機改動一樣推得上去。 部署內容與 git 歷史之間沒有強制連結,部署前請確認
工作樹是你想要的狀態(
git status)。 - 沒有紀錄說「線上這一版對應哪個 commit」。 想知道線上跑的是哪一版,查它的 NIP-11
version欄位(見下方「部署完怎麼確認」)。
① 有多個 Cloudflare 帳號時,wrangler 會挑錯
它可能打向你另一個帳號,回一個看起來像憑證壞掉的錯誤:
Authentication error [code: 10000]
憑證沒壞,是帳號選錯了。明示帳號 ID 就好(在 Cloudflare 後台首頁右側或網址列看得到):
CLOUDFLARE_ACCOUNT_ID=<你的帳號 ID> pnpm run deploy② 用 API token 部署時,權限要兩項不是一項
不想每次都開瀏覽器登入(或要放進自己的自動化)的話,用 API token:
CLOUDFLARE_API_TOKEN=<token> CLOUDFLARE_ACCOUNT_ID=<帳號 ID> pnpm run deploytoken 的權限要勾兩項,缺第二項會得到和上面一模一樣的 10000 錯誤:
| 權限 | 為什麼需要 |
|---|---|
| Workers Scripts: Edit | 上傳 Worker |
| User → Memberships: Read | wrangler 會查你在該帳號的角色 |
部署指令回報成功不等於站真的活著。實測兩件事:
curl https://cinder-relay.<你的子網域>.workers.dev/healthz
# 預期:ok
curl -H "Accept: application/nostr+json" https://cinder-relay.<你的子網域>.workers.dev
# 預期:一份 JSON,其中 "version" 是你剛部署的版號/healthz 回純文字 ok。不要用首頁判斷——合體模式下首頁本來就會回網頁版,
看它回不回 HTML 是判斷不出中繼站死活的。
NIP-11 的 version 欄位是確認「線上跑的是不是我剛推的那一版」的唯一方式。
想要「一個網域、一次部署」的話,多打一個指令就好:
pnpm run deploy:unified # 在 relay/ 目錄下它會先建置網頁前端(apps/desktop,mode=unified)再連同中繼站一起部署。兩者的差別:
| 指令 | 結果 | 適合誰 |
|---|---|---|
pnpm run deploy |
只有中繼站(預設) | 想跑一座公共錨點的人 |
pnpm run deploy:unified |
中繼站 + 網頁前端 | 自架給自己和朋友用的人 |
🔴 合體部署必須設 run_worker_first = true([env.unified.assets] 裡已經設好了)。
Cloudflare Static Assets 預設是資產優先:請求先比對資產清單,命中就直接回檔案,
Worker 的程式碼根本不會執行——而中繼站的入口 / 正好命中 index.html。少了這一行,
/ 會回 200 與一段 HTML,客戶端不會看到錯誤,只會看到握不了手的 WebSocket。
同樣的理由,合體部署後請用 GET /healthz(純文字 ok)判斷中繼站死活,不要再看 /。
relay 講標準 Nostr 的 REQ / EVENT / CLOSE 協定,可用 @cinderous/core 的 RelayClient:
import { RelayClient, createHeartbeat, generateSecretKey } from "@cinderous/core";
const ws = new WebSocket("wss://cinder-relay.<你的帳號>.workers.dev");
const sk = generateSecretKey();
const client = new RelayClient(
{ send: (data) => ws.send(data) },
{ onEvent: (subId, event) => console.log("收到", subId, event) },
);
ws.addEventListener("message", (m) => client.receive(m.data));
ws.addEventListener("open", () => {
client.subscribe("presence", [{ kinds: [20000] }]); // 訂閱上線心跳
client.publish(createHeartbeat(sk)); // 廣播自己的心跳
});桌面/前端可把這個 wss:// 位址設定為連線端點(見 apps/desktop/src/relay-source.ts)。
Ephemeral 心跳會隨上線人數扇出,請參考 docs/adr/0006
的容量估算(免費層約可支撐數十位並行使用者,並列出心跳間隔/合併/抖動等可調旋鈕)。
離線留言需要持久化。DO 已是 SQLite-backed(wrangler.toml 的 new_sqlite_classes);於 worker.ts 把
RelayCore 接上以 DO SQLite 為後備的 MessageStore(行為已由 relay/src/message-store.ts
定義並測試:NIP-40 過期、每收件人配額、以 #p 為索引)。詳見
docs/adr/0005。
本專案的開發硬規則如下:
- 單一真實來源(SSOT):產品需求看
PRD.md,模組與資料流看ARCHITECTURE.md。 - Architecture First / Search First:先定位模組、再讀現有實作、最後才動手。
- Fix First:延伸既有設計,不建立
v2、new_*、*_enhanced平行路徑。 - TDD:功能程式碼遵循 Red → Green → Refactor,測試即文件。
- 本地優先與低延遲:任何變更都不得破壞訊息即時性與隱私預設。
歡迎參與!送 PR 前請留意:
- 先讀
PRD.md(產品規格)、ARCHITECTURE.md(模組與資料流)與docs/adr/(既有決策)。 - 功能程式碼採 TDD;送交前確保
pnpm -r typecheck與pnpm -r test全綠(CI 也會檢查)。 - 架構/協定/隱私取捨等決策,請新增一份 ADR(格式見
docs/adr/0000-template.md)。 - 隱私是第一原則:任何變更都不得讓明文或私鑰離開裝置,或在未經 Gift Wrap 隱藏的情況下洩漏社交圖譜。
提交即表示你同意以本專案授權(AGPL-3.0)釋出你的貢獻。
這是一套處理端到端加密與私鑰的軟體,目前仍在開發階段、尚未經過安全稽核,請勿用於高風險場景。
發現安全問題請以私下管道回報,不要開公開 issue。完整的安全政策、加密盤點、威脅模型逐項盤點與已知限制清單見 docs/SECURITY.md。
本專案採用 GNU Affero General Public License v3.0(AGPL-3.0)——詳見 LICENSE。
這代表你可以自由使用、修改與散布;但若你散布修改版,或將修改版作為網路服務(例如自架本中繼站)提供給他人, 就必須以相同授權公開你的原始碼(AGPL 第 13 條)。此選擇是為了確保所有衍生版本與自架的 relay 都對使用者保持透明、可審計,貫徹本專案的隱私與去中心化精神。
Copyright (C) 2026 Cinderous contributors.