Skip to content

feat: スマホアプリ + E2E暗号化リアルタイムクラウド同期(Capacitor + Cloudflare relay)#8

Merged
1llum1n4t1s merged 22 commits into
mainfrom
feature/relay-sync
Jun 23, 2026
Merged

feat: スマホアプリ + E2E暗号化リアルタイムクラウド同期(Capacitor + Cloudflare relay)#8
1llum1n4t1s merged 22 commits into
mainfrom
feature/relay-sync

Conversation

@1llum1n4t1s

Copy link
Copy Markdown
Owner

概要

ぺたりんに ① スマホアプリ(Capacitor)② Notion 級のリアルタイム・クラウド同期(E2E 暗号化) を足す大型機能ブランチ。
同期は完全 opt-in で、既定 OFF=外部送信ゼロ・現状と完全に同一挙動を維持する。chrome.storage.sync を使う既存の「Chrome 標準同期」も挙動不変。

設計は「コスト最優先」「サーバーは全盲(zero-knowledge)」を 2 大原則に据え、Cloudflare の公式料金・ハイバネ WS 仕様を実証してから組んだ。

何が入ったか

同期エンジンの土台(挙動不変リファクタ)

  • chrome.storage.sync 直叩きを transport 抽象setSyncTransport/getSyncTransport)へ括り出し。既定は従来通りの chrome transport で挙動完全不変(回帰 PASS 維持)。

Cloudflare relay(サーバー側・infra/cloudflare/relay/

  • notify-then-pull アーキ: vault 単位の Durable Object を fan-out ハブにし、各端末は Hibernatable WebSocketstate.acceptWebSocket())を 1 本常時接続。アイドルは duration 課金ゼロ(CF 公式保証)。編集→本体 push→薄い変更ping だけ WS broadcast→受信端末は該当ドメインだけ pull→既存 mergeDomainNotes で反映。
  • サーバーは全盲: 本文は AES-GCM で E2E 暗号化、ドメイン名も HMAC ハッシュ化。relay は暗号文しか持たない。認証は ECDSA P-256 署名(無アカウント・公開鍵 first-write-wins)
  • D1(petarin-sync)+ SALT secret 投入済み、https://petarin-relay.1llum1n4t1.workers.dev にデプロイ済み。CORS 対応(manifest に origin を焼かず拡張 SW / モバイル WebView から叩ける)。

排他 3 モード同期(拡張クライアント)

  • 同期は OFF / Chrome 標準 / クラウド の排他 3 モード(並列=二重同期は衝突地獄なので不可)。syncMode で transport を切替。
  • クライアント側 vault 暗号モジュール(鍵ペア生成・HKDF→AES-GCM・item 暗号化・署名)+ RelayTransport(「暗号化された chrome.storage.sync ミラー」として sync.js を無改造で動かす)。
  • background が WS 保持・変更ピン受信→該当ドメインだけ reconcile・SW 休止からの復帰で張り直し&取りこぼし回収。未ペアリングの cloud は停止して誤って chrome.storage.sync へ漏らさない。
  • manage の同期パネルを 3 ラジオ化+モード別同意文+クラウドのペアリング UI(作成 / 参加 / 解除 / QR 表示 / コピー)。

スマホアプリ(mobile/・Capacitor)

  • Vite で web ビルド → @shared../src/shared同期エンジンを単一ソース再利用(コピーしない)。chrome.storage シム(Capacitor Preferences backed)+ モバイル版 sync オーケストレータ。
  • アプリ内 QR カメラスキャナ(getUserMedia + jsQR)で「撮って即参加」。ペアリングは QR 表示も追加(拡張デスク・モバイル両対応)。
  • 無課金スタンドアローン CRUD: 課金しなくても付箋アプリとして完結(FAB→グループ選択→本文/色9種/絵文字/Markdownプレビュー/文字数→保存・削除、ゴミ箱の復元/完全削除/空に)。グループは sync 安全な domain キー(group:base64url(NFC(name)))へ符号化=後でクラウド同期を買っても isValidDomain 無改修で通る。IAP(¥500 買い切り)はクラウド同期だけをゲート
  • Android デバッグ APK の CI(.github/workflows/mobile-android.yml)。

テスト(全て依存なしの決定的スイート・PASS 済み)

  • node scripts/_sync_repro.mjs223 PASS(同期エンジンの墓石/容量/設定マージ。storage/sync は無改修)
  • node scripts/_vault_selftest.mjs7 PASS(署名がサーバー検証を通る契約一致・AES-GCM round-trip・ハッシュ決定性)
  • node scripts/_relay_e2e.mjs7 PASS(実 DO/D1/Worker で notify-then-pull・暗号化往復・WS 変更ピン・削除・無認証 401)
  • node scripts/_mobile_sync_repro.mjs9 PASS(シム+実エンジン+実 relay で device A push→device B pull の暗号化往復)
  • node scripts/_mobile_crud_repro.mjs15 PASS(vault/同期なしのスタンドアローン CRUD データ経路・グループキーの同期安全性)
  • vite build(mobile)→ 26 modules OK

レビュアーへの注意 / マージ前の残作業

  • セキュリティ: relay は全盲設計(E2E 暗号・ドメイン HMAC)。SALT は CF secret(リポジトリに無し)。vault 秘密鍵は端末 local 専用キー(never sync)=紛失=復旧不可。
  • プライバシー文言: 同期 ON ではゴミ箱内の削除済み付箋本文も暗号化して cloud へ送る。リリース前に Google / Microsoft / Firefox 向けの同期文言をその旨含めて改訂すること。
  • 本番ハードニング(go-live 前): custom domain(relay.petarin.nephilim.jp 想定)へ寄せて workers_dev=false、unsafe ratelimit binding を再有効化(ローカル dev では workerd が落ちるためコメントアウト中)。
  • モバイル残: IAP プラグインのネイティブ配線(product jp.nephilim.petarin.sync non-consumable)、cap add android/ios+ネイティブビルド(iOS は macOS+署名・カメラに NSCameraUsageDescription 要)、ストア申請。
  • 既定 OFF のままなら現行ユーザーの挙動は一切変わらない(外部送信ゼロ)。

🤖 Generated with Claude Code

1llum1n4t1s and others added 15 commits June 23, 2026 00:40
リアルタイム relay 同期(A→C→B)の地ならし(A)。sync.js の chrome.storage.sync
直接呼び出し6箇所(機能検出 / readSync の getAll / push の set·remove)を transport
インターフェース(isAvailable/getAll/set/remove)へ集約。既定の ChromeSyncTransport は
メソッド内で呼び出し時に chrome を解決し現状と完全同一に振る舞う(読込時に参照を
キャプチャしない=テストの後付けモック互換)。setSyncTransport で将来 RelayTransport
(Cloudflare Worker 経由の HTTP)へ差し替えるための境界を用意。

挙動は不変。_sync_repro.mjs 195 PASS / 0 FAIL を維持。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
クラウド同期モード用 Cloudflare Workers リレーの server 側を新規スキャフォルド。
notify-then-pull(store-and-forward): vault 単位の VaultDO(Hibernatable WebSocket)が
暗号文ストア窓口(D1: petarin-sync) + per-vault seq 採番 + 変更ピンの fan-out を担う。

- src/index.ts: 薄い router。vaultId を SALT 付き SHA-256 でハッシュ化して DO を引く(横入り防止)。
- src/vault-do.ts: WS(acceptWebSocket=ハイバネ)/push/pull/catchup。push で D1 upsert→
  seq 採番→他端末へ {t:changed,d,seq} broadcast(送信WS無料)。
- src/auth.ts: 自己完結ペアリング鍵。vault の ECDSA P-256 公開鍵を first-write-wins 登録し
  以降のリクエストを署名検証(秘密はサーバーに無い)。本文は端末側 vaultKey で AES-GCM 暗号化、
  ドメイン名も HMAC ハッシュ化=サーバーは中身もサイトも知らない。
- migrations/0001_init.sql + wrangler.toml + README + tsconfig + package.json/lock。

Ferry-relay の流儀(DO+ハイバネ/SALT ハッシュ/Custom Domain のみ/Rate Limit 三段)に倣う。
topology は別(あちらは 2 peer 生パススルー、こちらは N端末 store-and-forward)。
tsc --noEmit 通過。未デプロイ(D1 作成・SALT・ドメイン確定は明示 GO 後)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A の setSyncTransport に差す客側 transport を実装。リレーを「暗号化された
chrome.storage.sync ミラー」として見せ、sync.js のマージ頭脳を無改造で動かす。

- src/shared/vault.js: vault 識別子と暗号プリミティブ。ECDSA P-256 鍵ペア(QR で端末間共有)/
  vaultKey→HKDF で AES-GCM・HMAC 派生 / item 暗号化(平文に元キーを内包)/ ドメイン HMAC ハッシュ /
  リクエスト署名(relay auth.ts と一致) / pairing 入出力。
- src/shared/relay-transport.js: isAvailable/getAll(/dump)/set(/push)/remove(/item DELETE) を
  署名付きで実装。値は AES-GCM 暗号化・キーは HMAC ハッシュ。getLastSeq を realtime 用に公開。
- relay(vault-do.ts): getAll 用 GET /dump と remove 用 DELETE /item を追加(seq 進行 + broadcast)。
- scripts/_vault_selftest.mjs: 暗号 round-trip・ハッシュ決定性・「署名が relay 検証を通る」契約を
  Node で検証(7 PASS)。relay tsc --noEmit 通過。

B3 で background の WS 配線・モード選択・ペアリング UI を繋ぎ、デプロイ後に実機疎通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- petarin-sync D1(903b1fb1…・APAC)を作成し remote にスキーマ適用。database_id を wrangler.toml へ。
- scripts/_relay_e2e.mjs: vault.js + relay-transport.js で set→dump→WS変更ピン→remove を実通しする
  e2e(リレーは wrangler dev か deploy 後の URL を RELAY_URL で指定)。
- .gitignore に .wrangler-state/ を追加。pnpm の build 許可を onlyBuiltDependencies に。
- ローカル workerd が std::terminate で起動不能(この Windows 環境固有・ratelimit 無関係)。
  unsafe ratelimit binding はローカル dev でコメントアウト(本番デプロイ時に再有効化)。
  ランタイム検証はデプロイ後に実機 e2e で行う。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- petarin-relay を CF へデプロイ(https://petarin-relay.1llum1n4t1.workers.dev)。実 Durable
  Object(ハイバネ WS) + 実 D1 + Worker で notify-then-pull が動作。
- 実機 e2e 7 PASS: 暗号化KVミラー往復 / meta往復 / WS変更ピン受信 / 削除 / 無認証401。
- SALT を本番 secret として投入(vaultId ハッシュ化・横入り防止)。
- ローカル workerd が当環境で起動不能(std::terminate)のため検証はデプロイ実機で実施。
- workers.dev は開発中の一時公開。本番 go-live で Custom Domain + workers_dev=false へ戻す。
  unsafe ratelimit binding も本番で再有効化する(local dev では workerd が落ちるためコメントアウト中)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
relay URL は環境で変わる(dev=workers.dev / 本番=custom domain)ため、クライアント側
manifest に origin を焼かず relay 側で CORS を開ける。認証は署名(X-Vault-*)で行い Cookie を
使わないので ACAO は "*"。OPTIONS preflight は 204、WS upgrade(101)は webSocket 保持のため
再構築せず CORS も付けない。fetch を handle() に分離。デプロイ + e2e 7 PASS / CORS 実レス確認済。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
排他3モード(off/chrome/cloud)を syncEnabled + syncMode の二段で表現(sync.js の syncEnabled
ゲートは不変)。transport 選択は background が syncMode で行い、cloud のとき relay-transport へ
差し替える(sync.js のマージ頭脳は無改造)。

- storage.js: DEFAULT_SETTINGS に syncMode 追加 + vault 鍵束の local 専用キー(petarin:sync:vault・
  never sync)と get/save/clear ヘルパ。秘密(署名秘密鍵JWK)を含むため chrome.storage.local のみ。
- background.js: applyTransport() で cloud+ペアリング済みなら setSyncTransport(relay)。realtime WS
  (/sync・"ping"/"pong" keepalive・指数backoff 再接続)で変更ピン {t:changed} を受けて pull。SW 休止
  対策に keepalive alarm(1分)で WS 張り直し+取りこぼし reconcile。cloud は巨大 budget で容量ロジック無効化。
- manifest(両): alarms 権限。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
付箋デスクの同期パネルを「ON/OFF トグル」から「同期しない / ブラウザ標準同期 / クラウド同期」の
排他3ラジオへ。mode 別の同意文を出し分け、クラウド時だけペアリング section を表示する。

- 作成: generateVault → ローカル保存 → 引き継ぎコード(exportPairingCode)を表示。
- 参加: コードを貼り付け → importVault で検証 → 保存。
- 解除: clearVaultPairing + purgeSync(shadow撤去)。コピー/再表示も。
- 100KB ゲージは容量上限のある chrome 標準同期のみ表示(cloud はリレー側で広いので隠す)。
- relay-transport に DEFAULT_RELAY_URL を追加(新規作成の初期リレー。本番は custom domain へ)。
- background: cloud だが未ペアリングのとき chrome へ漏らさず止める UNAVAILABLE_TRANSPORT。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capacitor アプリで拡張の同期エンジン(shared/storage.js・sync.js)を無改造で動かす土台。
chrome.storage.local / onChanged を 1 プロセス KV で再現(バックエンド注入式: Capacitor は
Preferences、テストはインメモリ)。get(null|str|[]|{}) / set 構造化クローン / Promise / onChanged 発火。

scripts/_mobile_sync_repro.mjs: device A が付箋を push → device B(別 backend・同 vault)が pull して
本文(改行)・icon・color が暗号化往復で一致することを実 relay 相手に確認。拡張側で未検証だった
「実エンジン+実 relay の cloud 往復」もこれで closing。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
クラウド同期(買い切り¥500)で PC とリアルタイム共有するスマホアプリの土台。Vite で web をビルドし
@shared エイリアスで拡張の同期エンジン(vault/sync/relay-transport/storage/markdown)を**単一ソース再利用**
(コピーしない)。`vite build` 成功確認(21 modules・44.5kB)。

- src/main.js: chrome.storage シム差し込み→エンジン起動→付箋一覧(Markdownプレビュー)+同期/ペアリングUI。
  モバイルは off/cloud の2択(chrome 標準同期は無い)。cloud は IAP 解禁ゲート。
- src/sync-orchestrator.js: background.js のモバイル版(reconcile デバウンス + realtime WS "ping"/"pong" +
  resume/pause で WS 張り直し/畳み)。cloud は巨大 budget で容量ロジック無効化。
- src/preferences-backend.js: シムの裏付け(Capacitor Preferences)。
- src/iap.js: 買い切り解禁ゲート(ネイティブ配線は TODO・ブラウザ dev は localStorage 解錠)。
- vite.config/capacitor.config/package.json + Android CI(ubuntu/JDK17/Gradle・debug APK)。android/ios は cap add 生成物=gitignore。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crypto.subtle(vault.js の暗号化)は secure context 限定で、LAN IP への素の HTTP は secure 扱い
されない=ペアリング/暗号化がこける。@vitejs/plugin-basic-ssl で自己署名 HTTPS を張り、server.host=true
で 0.0.0.0 bind。実機は証明書警告を一度許可すれば secure context になり crypto が動く。
LAN 配信を実機相当(curl -k https://<LAN-IP>:5180)で 200 確認。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
長いペアリングコード(643字)の手打ち/コピペを避けるため、コードと一緒に QR を出す。スマホのカメラで
読み取れる。実績ある依存ゼロの qrcode-generator(Arase・MIT)を採用。

- src/vendor/qrcode.js: vendor(拡張は classic script で window.qrcode・CSP self 内)。vm でグローバル経路検証。
- manage: pairCodeBox に QR <img>(createDataURL の GIF data URL=innerHTML 不使用・CSP安全)。作成/表示時に描画。
- mobile: qrcode-generator を Vite import し pairLinked に QR。vite build 22 modules 成功。
- ECC=L・余白16px(規格の4モジュール)で画面スキャンの読み取り安定。85〜89×89 程度。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vite 5.4 の host チェックでトンネル(*.trycloudflare.com 等)が弾かれるのを回避(dev のみ)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
標準カメラだと QR がただのテキスト→Web検索になる(URLスキーム無しのため正常)。アプリ内に
スキャナを持たせ、PC 拡張の QR を写すと parsePairingCode→importVault 検証→自動参加まで通す。

- index.html: 「📷 QRをスキャンして参加」ボタン + カメラパネル(video)。
- main.js: getUserMedia(environment) → requestAnimationFrame で jsQR デコード → 検出で onJoin 再利用。
  停止時は track 全停止。Web(Safari)は HTTPS=secure context で動作。ネイティブは Info.plist の
  NSCameraUsageDescription / 必要なら barcode-scanner プラグインへ差し替え(TODO)。
- vite build 成功(jsqr 同梱・197KB/gzip72KB)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
スマホアプリを課金なしで完結利用できる状態にする。クラウド同期(IAP)は
あくまで上乗せで、無課金でもローカル付箋アプリとして全機能が使える。

- notes-meta.js: content.js 由来の ICONS/pickIcon/clamp を複製し、
  スタンドアローンの「グループ」を sync 安全な domain キー
  (group:base64url(NFC(name))) へ符号化。後でクラウド同期を買っても
  sync.js の isValidDomain を無改修で通る/拡張のホスト名キーと名前空間分離。
- main.js: storage.js の既存経路(restoreNotes/updateNote/deleteNote/
  getTrash/restoreFromTrash/purgeFromTrash)を使い、FAB→作成先グループ選択
  →エディタ(本文/色/絵文字/Markdownプレビュー/文字数)→保存/削除、
  ゴミ箱ビュー(復元/完全削除/空に)を実装。カード色は CSS クラス非依存で
  JS 直塗り(9色対応)。同期/ペアリング/QR スキャンは従来通り温存。
- index.html: +FAB・ゴミ箱ボタン・エディタ/絵文字/グループ選択シートと CSS を追加。
- scripts/_mobile_crud_repro.mjs: vault/同期なしの CRUD データ経路 e2e(15 PASS)。
  グループキーが isValidDomain を通る=クラウド同期安全も検証。

検証: _mobile_crud_repro.mjs 15 PASS / _sync_repro.mjs 223 PASS(storage/sync 無改修) / vite build 26 modules OK。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4af6b44a-fba4-4b96-8cb0-49ebf597cd67

📥 Commits

Reviewing files that changed from the base of the PR and between fb47911 and e92c177.

📒 Files selected for processing (4)
  • docs/privacy-policy.en.md
  • docs/privacy-policy.md
  • infra/cloudflare/relay/wrangler.toml
  • src/shared/relay-transport.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • infra/cloudflare/relay/wrangler.toml
  • src/shared/relay-transport.js

📝 Walkthrough

Summary by CodeRabbit

リリースノート

  • New Features
    • クラウド同期(off/Chrome/クラウド)とエンド・ツー・エンド暗号化同期を追加
    • モバイル(Capacitor)の同期基盤とペアリングUI(QR/コード入力)を提供
  • Chores
    • Android向けdebug APKの自動ビルドCIを追加
    • リレー/同期の自己テスト・E2E検証を追加
  • Documentation
    • プライバシーポリシーを更新(クラウド同期の送信条件を明確化)
  • Permissions
    • 拡張のアラーム権限を追加

Walkthrough

E2E暗号化クラウド同期機能をブラウザ拡張・Cloudflare Workersリレー・Capacitorモバイルアプリの3層にわたって追加した。WebCrypto(ECDSA/AES-GCM/HKDF)ベースのvault暗号プリミティブを共有ライブラリとして実装し、syncのtransport抽象化・リレーサーバーD1/Durable Object実装・管理UIペアリング機能・モバイルアプリ基盤を一括導入した。

Changes

クラウド同期・モバイルアプリ基盤の追加

Layer / File(s) Summary
vault暗号プリミティブ・ペアリングコード
src/shared/vault.js, src/vendor/qrcode.js
WebCrypto(ECDSA P-256/AES-GCM/HKDF/HMAC)によるvault生成・暗号化・署名・ペアリングコードのエンコード/デコード、およびQRコード生成ベンダーライブラリ(2297行)を実装。
Cloudflare Workerリレーサーバー
infra/cloudflare/relay/src/auth.ts, infra/cloudflare/relay/src/index.ts, infra/cloudflare/relay/src/vault-do.ts
WorkerエントリポイントでvaultIdをSALT連結SHA-256でハッシュ化してDurable Objectへ転送。VaultDOがECDSA署名検証・first-write-wins公開鍵登録・D1 notesテーブルへのupsert/delete・WebSocketブロードキャストを実装。
Relay設定・インフラ整備
infra/cloudflare/relay/migrations/0001_init.sql, infra/cloudflare/relay/wrangler.toml, infra/cloudflare/relay/tsconfig.json, infra/cloudflare/relay/package.json, infra/cloudflare/relay/README.md, infra/cloudflare/relay/.gitignore, infra/cloudflare/relay/pnpm-workspace.yaml
D1初期化・バインディング設定・型設定・依存管理・ドキュメント・除外ファイル定義を一括構成。
sync transport抽象・relayトランスポート
src/shared/sync.js, src/shared/relay-transport.js
setSyncTransport/getSyncTransportによるtransport差し替え抽象を導入。relay向け暗号化KVミラー(署名付きHTTP・復号エラー時全体失敗)を実装。
ストレージ拡張・vault管理
src/shared/storage.js
syncMode設定(off/chrome/cloud)とVAULT_KEY定数、getVaultPairing/saveVaultPairing/clearVaultPairing(chrome.storage.local専用)を追加。
グループキー正規化・表示名
src/shared/groups.js
NFC正規化・UTF-8・base64urlによるグループキー生成・検証・表示名復元ヘルパを実装。
拡張background.jsのcloud同期対応
src/background/background.js, manifest.json, manifest.firefox.json
syncModeキャッシュ・vault再構築・applyTransport()によるtransport切替・relay WebSocketリアルタイム受信・指数バックオフ再接続・keepalive alarmを追加。alarms権限をマニフェストに追加。
管理UI同期パネル・ペアリング
src/manage/manage.html, src/manage/manage.css, src/manage/manage.js
同期パネルをoff/chrome/cloudの3モードラジオ選択へ刷新。cloudペアリング(作成/参加/QR表示/コピー/解除)のUIとハンドラを実装。
モバイルストレージ・IAP・メタデータ
mobile/src/storage-shim.js, mobile/src/preferences-backend.js, mobile/src/iap.js, mobile/src/notes-meta.js
chrome.storage互換シム・Capacitor Preferencesバックエンド・IAP dev解錠・グループキーユーティリティと絵文字管理を実装。
モバイルUI・Capacitor設定
mobile/index.html, mobile/capacitor.config.json, mobile/package.json, mobile/vite.config.js, mobile/README.md, mobile/.gitignore
付箋エディタ・同期パネル・QRスキャンUI、Capacitorアプリ設定・Vite開発環境・スクリプト・ドキュメント。
モバイル同期オーケストレーター
mobile/src/sync-orchestrator.js
cloud/chrome/off切替・relay WebSocket接続・指数バックオフ再接続・ハートビート・reconcileデバウンス・UI通知の公開API(startSync/stopSync/syncState/attachStorageListener/setOnChange)を実装。
E2E検証・CI自動化
scripts/_vault_selftest.mjs, scripts/_relay_e2e.mjs, scripts/_mobile_sync_repro.mjs, scripts/_mobile_crud_repro.mjs, .github/workflows/mobile-android.yml
vault自己検証・relay e2e・モバイルCRUD/sync再現の4スクリプト、mobile-v*タグ起動のAndroid debug APKビルドCIワークフローを追加。
プライバシーポリシー更新
docs/privacy-policy.en.md, docs/privacy-policy.md
cloud syncの暗号化方式(AES-GCM)・鍵管理・zero-knowledge・ドメイン識別のHMAC・削除済みノート送信・復号鍵喪失時の不可復旧・ペアリング共有端末のみ同期を詳細化。

Sequence Diagram(s)

クライアント(拡張またはモバイル)からリレー経由で2台間にノートが同期される主要フロー:

sequenceDiagram
  participant ExtA as デバイスA<br/>拡張/モバイル
  participant BG as background.js<br/>sync-orchestrator.js
  participant RT as relay-transport.js
  participant Relay as Cloudflare Worker<br/>petarin-relay
  participant VaultDO as VaultDO<br/>Durable Object
  participant D1 as D1<br/>notes
  participant ExtB as デバイスB<br/>拡張/モバイル

  rect rgba(100, 149, 237, 0.5)
    note over ExtA,BG: ペアリング・transport設定
    ExtA->>BG: generateVault / importVault
    BG->>RT: createRelayTransport(vault)
    BG->>Relay: WebSocket /sync 接続
  end

  rect rgba(60, 179, 113, 0.5)
    note over BG,D1: デバイスAがノートをpush
    BG->>RT: reconcile → transport.set(encryptItem)
    RT->>Relay: PUT /push {domain_hash, ciphertext, nonce}
    Relay->>VaultDO: verify(ECDSA署名) → handlePush
    VaultDO->>D1: notes upsert(seq採番)
    VaultDO-->>ExtB: WS broadcast {t:"changed", seq}
  end

  rect rgba(255, 165, 0, 0.5)
    note over ExtB,D1: デバイスBがpullして反映
    ExtB->>RT: transport.getAll() → GET /dump
    RT->>Relay: GET /dump
    Relay->>VaultDO: verify → handleDump
    VaultDO->>D1: 全件取得
    D1-->>ExtB: {items} decryptItem → ローカルへ反映
  end
Loading

Possibly Related PRs

  • 1llum1n4t1s/Petarin#2: PC拡張のsync.jsコア同期エンジン(reconcile/tombstone/meta)の改良と、本PRのtransport抽象化による拡張が同一モジュール内で直接重複する。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed プルリクエストのタイトルは、追加される主要な機能(スマホアプリと E2E 暗号化リアルタイムクラウド同期)を簡潔に説明しており、変更内容を正確に反映している。
Description check ✅ Passed プルリクエストの説明は、実装内容(同期エンジン、Cloudflare relay、3 モード同期、スマホアプリ)と設計原則、テスト結果、残作業を詳細に記載しており、変更内容と密接に関連している。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/relay-sync

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a cloud synchronization feature for the petarin note-taking application, adding a Cloudflare Workers relay (petarin-relay) using Durable Objects and D1, a Capacitor-based mobile application sharing the same sync engine via a chrome.storage shim, and pairing UI integration. The code review identified several critical issues: potential infinite WebSocket reconnection loops in both the mobile and extension background orchestrators during intentional disconnections, a sequence tracking bug in the relay's handleCatchup when notes are physically deleted, and a serialization issue in the mobile storage shim's in-memory backend during initialization with non-string seed values.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread mobile/src/sync-orchestrator.js
Comment thread src/background/background.js
Comment thread infra/cloudflare/relay/src/vault-do.ts
Comment thread mobile/src/storage-shim.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: faf4be8f08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/manage/manage.js
Comment thread mobile/src/main.js Outdated
Comment thread src/shared/relay-transport.js
Comment thread mobile/src/main.js Outdated
Comment thread infra/cloudflare/relay/src/vault-do.ts Outdated
Comment thread infra/cloudflare/relay/src/vault-do.ts Outdated
Comment thread mobile/src/main.js
…ュー対応)

PR #8 の Gemini Code Assist 指摘 4 件を修正。

- WS 再接続(high・background.js / sync-orchestrator.js): onclose の scheduleReconnect()
  が `if (_ws===ws)` ガードの外にあり、closeSocket/closeRelaySocket が _ws=null 後に
  close() しても(onclose は非同期発火で _ws===ws が偽)再接続がスケジュールされていた。
  背面化・off/解除中も WS が張り直し続けバッテリ/帯域を浪費する。null 化と再接続を
  同じガード内へ入れ、意図的切断では再接続しないようにした。
- relay catchup の seq 前進(medium・vault-do.ts): handleDelete は行を物理削除するため
  `seq>since` のクエリに乗らず、削除だけの回はクライアントの since が進まず同じ since で
  無駄問い合わせを繰り返していた。返す seq を `Math.max(currentSeq, since)` にして前進させる。
  削除自体の伝播は墓石(meta item の push=changes に乗る)が担うので取りこぼさない
  (relay モードは巨大 budget で metaDeferred 無効=meta は必ず push される)。
  scripts/_relay_e2e.mjs に「削除だけでも catchup の seq が前進」する実機回帰を追加。
- storage-shim seed(medium): createMemoryBackend(seed) が生のオブジェクト/配列をそのまま
  Map に入れ、readRaw の JSON.parse が壊れていた。非文字列は JSON 文字列化して格納
  (Capacitor Preferences と同じ文字列 KV 契約に合わせる)。

検証: relay tsc OK / _sync_repro 223 PASS / _mobile_crud_repro 15 PASS /
_relay_e2e 9 PASS(catchup 追加) / _mobile_sync_repro 9 PASS / vite build 26 modules OK。
relay 再デプロイ済(Version 4729ca62・catchup 修正反映)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (4)
src/background/background.js-104-161 (1)

104-161: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

connectRelaySocket() に多重接続レースの可能性があります。

Line 106 の OPEN/CONNECTING ガード通過後、loadVault()signRequest()await(特に署名は WebCrypto で必ずマイクロ/マクロタスクを跨ぐ)を経てから Line 132 で _ws を代入します。この間に applyTransport()(settings 変更と vault 変更が連続発火)や keepalive alarm / 再接続タイマからもう一度呼ばれると、両方ともガードを抜けて new WebSocket を 2 本張ってしまいます。後勝ちで _ws が上書きされ、片方は onclose_ws === ws を満たさず再接続もされない孤児接続として残り、接続チャーンの原因になります。

接続中フラグで直列化することを推奨します。

🔒 多重接続を防ぐガードの例
 let _ws = null;
+let _wsConnecting = false;
 let _wsAttempt = 0;
 let _wsReconnectTimer = 0;
 let _heartbeatTimer = 0;

 async function connectRelaySocket() {
   if (!isCloudActive()) return;
+  if (_wsConnecting) return;
   if (_ws && (_ws.readyState === WebSocket.OPEN || _ws.readyState === WebSocket.CONNECTING)) return;
-  const vault = await loadVault();
-  if (!vault) return;
+  _wsConnecting = true;
+  try {
+    const vault = await loadVault();
+    if (!vault) return;
     // …(既存の WS 構築処理)…
+  } finally {
+    _wsConnecting = false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/background.js` around lines 104 - 161, The
connectRelaySocket() function has a race condition where multiple calls can pass
the initial guard on line 106 (checking _ws readyState) and then proceed through
the async operations (loadVault and signRequest). This causes multiple WebSocket
instances to be created, with the last one overwriting _ws and leaving orphaned
connections. Introduce a connecting flag (similar to how you might track
connection state) that guards the entire connection process from the start of
connectRelaySocket() through the assignment of _ws on line 132. Set this flag to
true at the beginning of the function before any async operations, and set it
back to false if any error occurs or at the end of successful connection setup.
This ensures only one connection attempt can be in progress at a time,
preventing the race condition.
infra/cloudflare/relay/README.md-63-68 (1)

63-68: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

「まだ無いもの」セクションが現状実装と食い違っています。

ここに記載されている RelayTransport / ペアリングUI / background 側WS連携は、このPRの目的と実装内容では導入済みです。運用時の誤解を避けるため、現況に合わせて更新してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/cloudflare/relay/README.md` around lines 63 - 68, The "まだ無いもの(B の続き)"
section in the README lists features that are no longer in a "not yet
implemented" state. Update this section to reflect the current implementation
status by removing or relocating the entries for RelayTransport, ペアリング UI with
manage sync panel mode selection, and background.js WS connection management
with domain reconciliation, as these features have been implemented in the
current PR. Ensure the documentation accurately represents what remains to be
done versus what is already completed.
infra/cloudflare/relay/README.md-14-17 (1)

14-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

フェンス付きコードブロックに言語指定を付けてください。

このブロックは markdownlint の MD040 警告対象です。text などを付ければ警告を解消できます。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/cloudflare/relay/README.md` around lines 14 - 17, The fenced code block
in the README.md file is missing a language specification after the opening
triple backticks, which triggers a markdownlint MD040 warning. Add a language
identifier such as `text` immediately after the opening triple backticks (change
``` to ```text) to resolve the warning and comply with markdown linting
standards.

Source: Linters/SAST tools

src/shared/vault.js-99-102 (1)

99-102: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

importVaultpairing.url を先に検証してください。

pairing.url が未検証のまま通るため、壊れたペアリングコードが「後段の通信失敗」としてしか見えません。インポート時に URL 構文と許可スキームを弾くと、失敗が即時かつ原因明確になります。

🛠️ 修正例
 export async function importVault(pairing) {
   if (!pairing || pairing.v !== 1 || !pairing.id || !pairing.k || !pairing.sk || !pairing.pk) {
     throw new Error("invalid pairing payload");
   }
+  let u;
+  try {
+    u = new URL(String(pairing.url || ""));
+  } catch {
+    throw new Error("invalid relay url");
+  }
+  const isLocalhost = u.hostname === "localhost" || u.hostname === "127.0.0.1";
+  if (u.protocol !== "https:" && !(u.protocol === "http:" && isLocalhost)) {
+    throw new Error("relay url must be https (or localhost http)");
+  }
   return buildVault(pairing, b64urlToBytes(pairing.k));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/vault.js` around lines 99 - 102, The `importVault` function
validates several pairing properties like `pairing.v`, `pairing.id`,
`pairing.k`, `pairing.sk`, and `pairing.pk` but does not validate `pairing.url`
in the validation check. Add validation for `pairing.url` in the same condition
that validates the other required pairing properties, ensuring the URL has valid
syntax and uses an allowed scheme. This will allow broken pairing codes to fail
immediately with a clear error message rather than failing later during
communication attempts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/mobile-android.yml:
- Around line 21-23: The setup-node@v4 action lacks an explicit cache
configuration which poses a security risk since future versions (v5/v6) may
enable caching by default in release workflows. In the setup-node@v4 step where
node-version is configured to 22, add an explicit package-manager-cache
parameter and set it to false to prevent potential cache poisoning attacks
during release processes and to lock in the intended behavior regardless of
future action version updates.
- Around line 19-20: The actions/checkout@v4 step in the mobile-android.yml
workflow is missing the persist-credentials configuration. Add the
persist-credentials: false parameter to the checkout action step to prevent Git
credentials from being persisted and reduce the attack surface for read-only
build operations. This is a security best practice to ensure that if a
downstream action is compromised, attackers cannot access the Git credentials.
- Line 19: Replace the variable version tag reference `@v4` in the
actions/checkout action with the full 40-character commit SHA to protect against
supply chain attacks. Pin all GitHub Actions referenced in the workflow file
(including those on lines 21, 28, and 50 as mentioned in the comment) to their
complete commit SHA instead of version tags like `@v4`. Add the version tag as a
comment next to the full SHA to maintain readability and ease future updates.

In `@infra/cloudflare/relay/pnpm-workspace.yaml`:
- Around line 1-4: The allowBuilds configuration in pnpm-workspace.yaml contains
placeholder text ("set this to true or false") for the esbuild, sharp, and
workerd packages instead of actual boolean values. Replace each placeholder
under the allowBuilds section with either true or false based on whether builds
should be allowed for those packages (esbuild, sharp, workerd). If build control
is not required, remove the entire allowBuilds block and any associated
onlyBuiltDependencies setting that may exist elsewhere in the file, as
onlyBuiltDependencies is deprecated in pnpm v11.

In `@infra/cloudflare/relay/src/index.ts`:
- Around line 51-54: The WebSocket detection in the isWs constant uses a
case-sensitive comparison against the Upgrade header value. Since HTTP header
tokens are case-insensitive per specification, the current strict equality check
against "websocket" will fail for variations like "WebSocket" or "WEBSOCKET",
causing WebSocket connections to be incorrectly treated as regular HTTP
requests. Convert the Upgrade header value to lowercase using toLowerCase()
before comparing it to "websocket" to ensure case-insensitive matching. Apply
the same fix to the other location referenced at line 63-64.
- Around line 76-83: The code uses env.SALT without validating it exists, which
means if the SALT environment variable is not configured, the identifier hashing
will fail silently and weaken security. Add explicit validation to check that
env.SALT is set before calling hashVaultId on line 76, and return a 500 error or
throw an error if SALT is missing. This ensures misconfiguration is detected
immediately rather than allowing the code to continue with an invalid or
undefined salt value.

In `@infra/cloudflare/relay/src/vault-do.ts`:
- Around line 65-71: The public key is being persisted to storage before
signature verification, creating a security vulnerability where an attacker
could submit a malicious public key and lock out legitimate users. Move the
`storage.put` call for the "pubkey" that occurs when storedPub is not found and
pubB64 is provided to execute only after the signature verification logic (which
happens in the subsequent lines) has completed successfully. Ensure that the
public key is only permanently stored after cryptographic validation confirms it
is authentic and trusted.

In `@mobile/index.html`:
- Around line 113-149: Several modal panels (editorPanel with id="edTitle",
iconPanel, groupPickPanel, and the panel at line 189) are missing accessibility
attributes that are already implemented in the syncPanel. For each of these
panels, add role="dialog" and aria-modal="true" to the sheet div element, and
add aria-labelledby pointing to the respective heading IDs (edTitle, iconPanel
heading, groupPickPanel heading, etc.) to ensure assistive technologies can
properly identify and announce the modal context. This applies to the sheet divs
within editorPanel, iconPanel, groupPickPanel, and the panel referenced at line
189.

In `@mobile/src/iap.js`:
- Around line 19-27: The development-mode unlock mechanism using DEV_UNLOCK_KEY
and localStorage is accessible in production environments, allowing unauthorized
unlocking of features without actual IAP transactions. In the initIap()
function, add an environment check to ensure the localStorage DEV_UNLOCK_KEY
flag is only trusted in development mode, while production mode ignores this
flag and relies solely on verified IAP purchases. Additionally, verify that the
purchase() function at line 33 and any other unlock mechanisms properly validate
actual IAP transactions in production instead of unconditionally setting
_unlocked to true. Separate development-only bypass logic explicitly from
production security logic.

In `@mobile/src/sync-orchestrator.js`:
- Around line 56-63: The applyTransport function does not handle vault switching
correctly because the early return condition at line 104 only checks for the
existence of the _ws WebSocket connection, not whether the vault has changed. To
fix this, maintain a variable that tracks the identifier or reference of the
currently connected vault, then in the applyTransport function compare the newly
loaded vault with the stored vault identifier. If they differ, explicitly
disconnect and close the old WebSocket connection before creating the new relay
transport with createRelayTransport and reconnecting via connectSocket. This
ensures old vault connections are properly replaced when switching vaults.
- Around line 73-90: The issue is that reconciliation requests arriving during
an active runReconcile() execution are discarded because the _reconciling guard
causes immediate return. To fix this, introduce a _pending flag that gets set to
true in scheduleReconcile() when _reconciling is already true, then in the
finally block of runReconcile(), check if _pending is true and if so, invoke
runReconcile() again after resetting the flag to false. This ensures that any
reconciliation requests that arrived during execution are handled once the
current execution completes.

In `@mobile/vite.config.js`:
- Around line 17-22: The allowedHosts property in the server configuration is
set to true, which creates a DNS Rebinding attack vulnerability by accepting
requests from any host. Replace the allowedHosts: true assignment with an
approach that explicitly enumerates allowed hosts using the
VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS environment variable. This ensures only
trusted hosts specified in the environment configuration are permitted to access
the development server, rather than blindly accepting all hosts.

In `@scripts/_mobile_crud_repro.mjs`:
- Line 32: The ok() assertion in the test at line 32 contains `|| true` which
causes the condition to always evaluate to true regardless of the actual
isValidDomain() result, making this assertion meaningless and unable to detect
failures. Remove the `|| true` portion from the ok() function call so that the
assertion properly verifies whether isValidDomain("仕事") actually returns true.

In `@scripts/_mobile_sync_repro.mjs`:
- Line 14: The RELAY constant currently defaults to a production Relay URL when
RELAY_URL environment variable is not set, which poses a risk of accidental
writes to production. Change the default behavior by either making RELAY_URL a
required environment variable (throwing an error if not provided) or setting
localhost as the default fallback instead of the production URL. This change
should also be applied consistently wherever the orphan notation references
RELAY_URL usage.

In `@src/shared/relay-transport.js`:
- Around line 55-60: The empty catch block in the decryptItem call silently
skips failed decryption, which causes the sync core to misinterpret missing
items as deletions rather than corruption. Instead of catching and ignoring
decryption failures in this try-catch block within the getAll operation, either
rethrow the error or establish an error flag that causes the entire operation to
fail and be retried. This ensures that corrupted or undecryptable items are not
silently dropped, preventing them from being incorrectly treated as deleted data
by the downstream sync logic.
- Around line 24-41: The req function makes a fetch call without any timeout
mechanism, which can cause the reconciliation process to hang indefinitely if
the server is unresponsive. Add an AbortController with a timeout to the req
function by creating an AbortController instance, setting a timeout that calls
controller.abort() after a specified duration (e.g., 30 seconds), and adding the
controller.signal property to the init object passed to the fetch call. This
ensures the fetch request will automatically abort and fail fast if it exceeds
the timeout period, preventing synchronization from being blocked.

In `@src/shared/sync.js`:
- Around line 465-473: The setSyncTransport() function allows dynamic
replacement of _transport at any time, but the _reconcile() function performs
long-running operations across multiple await calls. If setSyncTransport() is
called during _reconcile() execution, reads and writes could use different
backend transports, causing data corruption. To fix this, capture a snapshot of
the current _transport at the beginning of the _reconcile() function and use
that captured reference throughout the entire function instead of repeatedly
accessing the global _transport variable, ensuring all read/write operations
within a single reconciliation use the same backend.

---

Minor comments:
In `@infra/cloudflare/relay/README.md`:
- Around line 63-68: The "まだ無いもの(B の続き)" section in the README lists features
that are no longer in a "not yet implemented" state. Update this section to
reflect the current implementation status by removing or relocating the entries
for RelayTransport, ペアリング UI with manage sync panel mode selection, and
background.js WS connection management with domain reconciliation, as these
features have been implemented in the current PR. Ensure the documentation
accurately represents what remains to be done versus what is already completed.
- Around line 14-17: The fenced code block in the README.md file is missing a
language specification after the opening triple backticks, which triggers a
markdownlint MD040 warning. Add a language identifier such as `text` immediately
after the opening triple backticks (change ``` to ```text) to resolve the
warning and comply with markdown linting standards.

In `@src/background/background.js`:
- Around line 104-161: The connectRelaySocket() function has a race condition
where multiple calls can pass the initial guard on line 106 (checking _ws
readyState) and then proceed through the async operations (loadVault and
signRequest). This causes multiple WebSocket instances to be created, with the
last one overwriting _ws and leaving orphaned connections. Introduce a
connecting flag (similar to how you might track connection state) that guards
the entire connection process from the start of connectRelaySocket() through the
assignment of _ws on line 132. Set this flag to true at the beginning of the
function before any async operations, and set it back to false if any error
occurs or at the end of successful connection setup. This ensures only one
connection attempt can be in progress at a time, preventing the race condition.

In `@src/shared/vault.js`:
- Around line 99-102: The `importVault` function validates several pairing
properties like `pairing.v`, `pairing.id`, `pairing.k`, `pairing.sk`, and
`pairing.pk` but does not validate `pairing.url` in the validation check. Add
validation for `pairing.url` in the same condition that validates the other
required pairing properties, ensuring the URL has valid syntax and uses an
allowed scheme. This will allow broken pairing codes to fail immediately with a
clear error message rather than failing later during communication attempts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c107e87e-df64-4d89-9488-e958933e2751

📥 Commits

Reviewing files that changed from the base of the PR and between 58232aa and faf4be8.

⛔ Files ignored due to path filters (2)
  • infra/cloudflare/relay/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • mobile/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (38)
  • .github/workflows/mobile-android.yml
  • infra/cloudflare/relay/.gitignore
  • infra/cloudflare/relay/README.md
  • infra/cloudflare/relay/migrations/0001_init.sql
  • infra/cloudflare/relay/package.json
  • infra/cloudflare/relay/pnpm-workspace.yaml
  • infra/cloudflare/relay/src/auth.ts
  • infra/cloudflare/relay/src/index.ts
  • infra/cloudflare/relay/src/vault-do.ts
  • infra/cloudflare/relay/tsconfig.json
  • infra/cloudflare/relay/wrangler.toml
  • manifest.firefox.json
  • manifest.json
  • mobile/.gitignore
  • mobile/README.md
  • mobile/capacitor.config.json
  • mobile/index.html
  • mobile/package.json
  • mobile/src/iap.js
  • mobile/src/main.js
  • mobile/src/notes-meta.js
  • mobile/src/preferences-backend.js
  • mobile/src/storage-shim.js
  • mobile/src/sync-orchestrator.js
  • mobile/vite.config.js
  • scripts/_mobile_crud_repro.mjs
  • scripts/_mobile_sync_repro.mjs
  • scripts/_relay_e2e.mjs
  • scripts/_vault_selftest.mjs
  • src/background/background.js
  • src/manage/manage.css
  • src/manage/manage.html
  • src/manage/manage.js
  • src/shared/relay-transport.js
  • src/shared/storage.js
  • src/shared/sync.js
  • src/shared/vault.js
  • src/vendor/qrcode.js

Comment thread .github/workflows/mobile-android.yml Outdated
Comment thread .github/workflows/mobile-android.yml Outdated
Comment thread .github/workflows/mobile-android.yml Outdated
Comment thread infra/cloudflare/relay/pnpm-workspace.yaml Outdated
Comment thread infra/cloudflare/relay/src/index.ts Outdated
Comment thread scripts/_mobile_crud_repro.mjs Outdated
Comment thread scripts/_mobile_sync_repro.mjs Outdated
Comment thread src/shared/relay-transport.js
Comment thread src/shared/relay-transport.js
Comment thread src/shared/sync.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/_relay_e2e.mjs`:
- Around line 105-109: The fetch response from the `/catchup` endpoint is not
being validated for HTTP success status before calling r.json(), which can cause
unhandled exceptions if the server returns an error response. Add a check for
r.ok after the fetch call completes, and if it is false, throw an error with the
response status and status text included for better error diagnostics. Only call
r.json() if the response is successful.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08af127f-54d3-4ecc-b1ce-4d5fa52b6be1

📥 Commits

Reviewing files that changed from the base of the PR and between faf4be8 and 88b87e1.

📒 Files selected for processing (5)
  • infra/cloudflare/relay/src/vault-do.ts
  • mobile/src/storage-shim.js
  • mobile/src/sync-orchestrator.js
  • scripts/_relay_e2e.mjs
  • src/background/background.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/background/background.js
  • mobile/src/storage-shim.js
  • infra/cloudflare/relay/src/vault-do.ts
  • mobile/src/sync-orchestrator.js

Comment thread scripts/_relay_e2e.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88b87e1aa4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mobile/src/iap.js
Comment thread mobile/src/main.js
Comment thread mobile/src/sync-orchestrator.js
Comment thread infra/cloudflare/relay/src/vault-do.ts
Comment thread mobile/src/notes-meta.js Outdated
1llum1n4t1s and others added 3 commits June 23, 2026 17:34
CodeRabbit/Codex の指摘に対応(relay サーバー側)。
- vault-do.ts: 公開鍵を署名検証成功後にのみ first-write-wins 登録する。検証前に保存すると
  vaultId を知った第三者や壊れた初回リクエストが不正鍵を焼き付け、以降の正当クライアントを
  全て Bad signature でブリックできた(critical)。署名対象に HTTP のクエリ(url.search)を含め、
  DELETE /item?d=... の d 改竄で別ドメインのアイテムを削除できた脆弱性を塞ぐ(WS は認証
  パラメータ自体=sig を含むため署名対象外)。seq は get→put を原子的に確保してから D1 を
  操作し、D1 の await を跨ぐ concurrent request の seq 重複/飛びを防ぐ(push/delete 両方)。
- index.ts: SALT 未注入は fail-fast(500)で黙って弱状態で動かさない。Upgrade ヘッダ判定を
  toLowerCase で大小文字非依存に共通関数化(RFC 7230)。
- pnpm-workspace.yaml: pnpm の無効キー allowBuilds のプレースホルダを除去し、
  relay に無い sharp も外して onlyBuiltDependencies を確定値にする。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…8 レビュー対応)

CodeRabbit/Codex の指摘に対応(クライアント同期コア・共有・拡張)。
- vault.js: signRequest の署名対象に query を追加(relay auth.ts と一致=クエリ改竄防止)。
- relay-transport.js: req に AbortController 15s タイムアウト(遅延/ハングで reconcile・MV3 SW が
  長時間ブロックされないように)。getAll は復号失敗を黙ってスキップせず throw。不完全なミラーを
  返すと sync.js が「remote に無い=削除」と誤認し local 付箋を消すため、getAll 全体を失敗させ
  reconcile を中断して local を温存する(同一 vault の item は同 aesKey=復号失敗は破損の異常)。
- sync.js: _reconcile 冒頭で getSyncTransport() を 1 つ確定し、readSync/push まで同じ transport を
  使い切る。実行中に setSyncTransport() で差し替わると read と write が別バックエンドに割れる
  race(cloud↔chrome 切替時)を解消。
- groups.js(新規): グループキー(group:base64url)の符号化/デコードを拡張デスクとモバイルで共有。
- manage.js: クラウド同期で来た group: キーを decodeGroupName で表示し、group キーは https URL を
  持たないので「開く」ボタンを隠す(無効 URL 生成を防止)。
- background.js: WS 署名にクエリ引数("")を渡す(署名フォーマット一致)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…CI(PR#8 レビュー対応)

CodeRabbit/Codex の指摘に対応(モバイル+テスト/CI)。
- main.js: onCreate/onJoin で syncScope:"all" を明示(mobile はドメイン選択 UI が無く、既定
  "selected"+空 syncDomains だと同期対象がゼロで動かない)。ペアリング済みでも保存済み pairing
  からコード/QR を再表示(別端末を追加招待でき、onCopy の空コピーも防ぐ)。onUnlink で
  syncEnabled:false(vault 喪失で実質停止する状態と UI の「同期ON・未接続」矛盾を解消)。
- sync-orchestrator.js: vault stamp で vault 切替時に WS を張り直す(旧 vault に居残らない)。
  reconcile 実行中に来た要求を pending で完了後 1 回追走(取りこぼし防止)。WS 署名にクエリ引数。
- iap.js: import.meta.env.DEV で本番ビルドの DEV 解錠バイパスを封じる(DevTools での無課金解錠防止)。
- index.html: editor/icon/groupPick モーダルに role=dialog/aria-modal/aria-labelledby を付与。
- vite.config.js: allowedHosts を *.trycloudflare.com 既定許可+VITE_ALLOWED_HOSTS 追加に変更
  (true の任意ホスト許可=DNS rebinding を回避・dev サーバのみ)。
- notes-meta.js: グループキーを src/shared/groups.js から再エクスポート(実体を共有)。
- mobile-android.yml: Actions を full commit SHA にピン留め・persist-credentials:false・
  package-manager-cache:false・cap sync 後に AndroidManifest へ CAMERA 権限注入(QR スキャナ)。
- scripts: _mobile_sync_repro は RELAY_URL を必須化(本番への誤書き込み防止)。
  _mobile_crud_repro の `|| true` 無効アサーションを実検証に修正。_relay_e2e は catchup の r.ok
  検証と署名クエリを追加。_vault_selftest は署名フォーマット変更(query)に追従。

検証: relay tsc OK / _vault_selftest 7 / _sync_repro 223 / _mobile_crud_repro 15 /
_relay_e2e 9 / _mobile_sync_repro 9 / vite build OK。relay 再デプロイ済(Version 1f8392d4)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

manage.js の onModeChange で chrome↔cloud 切替時、前バックエンドとの合意状態(shadow)が
残ったまま新バックエンドで reconcile すると、mergeDomainNotes が「shadow にあるが新 remote に
無い=リモート削除」と誤判定し local 付箋を消す危険があった。off と同様、chrome/cloud へ
切り替えるときもモード確定前に petarin:purgeSync で投影を撤去し、新バックエンドでは base=空から
和集合マージ(何も消さない)させる。CodeRabbit #3457616122。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/shared/relay-transport.js (1)

23-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Line 23 のコメントが変更後の挙動と矛盾しています(署名対象は pathname のみではなくなった)。

q(クエリ)を signRequest の署名対象に含めるようになり、relay 側 verifyquery = url.search)と一致するようになりました。しかし Line 23 のコメントは「path はクエリ無し(署名対象 = pathname のみ。relay auth.ts と一致)」のままで、現状の改竄防止モデル(?d=... 改竄による別ドメインアイテム削除の防止)を誤って説明しています。将来の誤読を避けるためコメントを更新してください。

📝 コメント更新例
-  // 署名付きで relay を叩く。path はクエリ無し(署名対象 = pathname のみ。relay auth.ts と一致)。
+  // 署名付きで relay を叩く。署名対象は pathname に加えクエリ(?d=... 等)も含める
+  // (relay verify の url.search と一致=クエリ改竄による別アイテム削除を防止)。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/relay-transport.js` around lines 23 - 28, The comment above the
req function states that the signature target is pathname only, but the actual
signRequest call now includes the query parameter q alongside the pathname.
Update the comment to accurately describe that the signature covers both the
pathname and query parameters (including the q variable constructed from the
query input), explaining that this prevents tampering with query strings such as
domain-related parameters that could lead to unauthorized operations on other
domains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/shared/relay-transport.js`:
- Around line 23-28: The comment above the req function states that the
signature target is pathname only, but the actual signRequest call now includes
the query parameter q alongside the pathname. Update the comment to accurately
describe that the signature covers both the pathname and query parameters
(including the q variable constructed from the query input), explaining that
this prevents tampering with query strings such as domain-related parameters
that could lead to unauthorized operations on other domains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60315452-1442-4d7d-aec7-9bf771d2c190

📥 Commits

Reviewing files that changed from the base of the PR and between 88b87e1 and fb47911.

📒 Files selected for processing (21)
  • .github/workflows/mobile-android.yml
  • infra/cloudflare/relay/pnpm-workspace.yaml
  • infra/cloudflare/relay/src/auth.ts
  • infra/cloudflare/relay/src/index.ts
  • infra/cloudflare/relay/src/vault-do.ts
  • mobile/index.html
  • mobile/src/iap.js
  • mobile/src/main.js
  • mobile/src/notes-meta.js
  • mobile/src/sync-orchestrator.js
  • mobile/vite.config.js
  • scripts/_mobile_crud_repro.mjs
  • scripts/_mobile_sync_repro.mjs
  • scripts/_relay_e2e.mjs
  • scripts/_vault_selftest.mjs
  • src/background/background.js
  • src/manage/manage.js
  • src/shared/groups.js
  • src/shared/relay-transport.js
  • src/shared/sync.js
  • src/shared/vault.js
🚧 Files skipped from review as they are similar to previous changes (14)
  • infra/cloudflare/relay/src/auth.ts
  • mobile/src/iap.js
  • scripts/_vault_selftest.mjs
  • scripts/_mobile_crud_repro.mjs
  • .github/workflows/mobile-android.yml
  • infra/cloudflare/relay/src/index.ts
  • src/manage/manage.js
  • mobile/index.html
  • src/shared/sync.js
  • scripts/_relay_e2e.mjs
  • mobile/src/sync-orchestrator.js
  • infra/cloudflare/relay/src/vault-do.ts
  • src/background/background.js
  • scripts/_mobile_sync_repro.mjs

…mit 有効化

実機確認以外の残タスクとして relay を本番構成へ移行。
- wrangler.toml: custom domain relay.petarin.nephilim.jp を routes に追加(nephilim.jp は CF Zone で
  証明書自動発行)、workers_dev=false で workers.dev の口を閉じる、unsafe ratelimit binding
  (IP 60/min・vault 120/min)を再有効化。
- relay-transport.js: DEFAULT_RELAY_URL を custom domain に変更(新規 vault はこの URL を使う)。

検証: RELAY_URL=https://relay.petarin.nephilim.jp で _relay_e2e 9 PASS / _mobile_sync_repro 9 PASS。
workers.dev/health は 404(閉鎖確認)。Version 8b2c00da。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
同期 ON ではゴミ箱内の削除済み付箋本文も中継サーバーへ送られるため、日英のポリシーを改訂。
同期を「A. ブラウザ標準同期(無料)」「B. クラウド同期(買い切り・E2E暗号化・ゼロ知識)」の2方式に
整理し、B では本文を端末で AES-GCM 暗号化・ドメイン名を HMAC ハッシュ化して中継し開発者は内容を
読めないこと、ゴミ箱本文も同様に暗号化送信されることを明記。「開発者サーバーへ送信しない」も
「クラウド同期をオンにした場合を除く」に修正。最終更新日を 2026-06-23 に更新。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant