Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e1abbe8
refactor(sync): chrome.storage.sync 直叩きを transport 抽象へ括り出す(挙動不変)
1llum1n4t1s Jun 22, 2026
18ac127
feat(relay): 同期リレーの足場(B1) — Worker + VaultDO + D1 スキーマ
1llum1n4t1s Jun 22, 2026
012f636
feat(relay): 客側 RelayTransport + vault 暗号モジュール(B2)
1llum1n4t1s Jun 22, 2026
b6cb195
chore(relay): petarin-sync D1 を作成し wrangler.toml に紐付け + ローカル e2e 整備
1llum1n4t1s Jun 22, 2026
c4f31ba
chore(relay): CF へデプロイし実機 e2e で検証(7 PASS) + SALT 投入
1llum1n4t1s Jun 22, 2026
1edc857
feat(relay): CORS を返して拡張SW/モバイルWebViewから cross-origin で叩けるように
1llum1n4t1s Jun 22, 2026
90efded
feat(sync): 拡張クライアントの cloud モード配線(B3 基盤)
1llum1n4t1s Jun 22, 2026
be66154
feat(manage): 同期パネルを排他3モード化 + クラウドのペアリングUI(B3)
1llum1n4t1s Jun 22, 2026
d189a0f
feat(mobile): chrome.storage シム + モバイル同期パスの e2e 証明(9 PASS)
1llum1n4t1s Jun 22, 2026
1941544
feat(mobile): Capacitor アプリの骨組み(同期エンジン再利用・ペアリングUI・Android CI)
1llum1n4t1s Jun 22, 2026
bf1898a
chore(mobile): dev サーバを HTTPS+LAN 公開化(実機 iPhone/Android テスト用)
1llum1n4t1s Jun 22, 2026
5dfb5c4
feat(sync): ペアリングにQRコード表示を追加(拡張デスク・モバイル両対応)
1llum1n4t1s Jun 22, 2026
22bb6ea
chore(mobile): dev サーバで allowedHosts=true(トンネル経由の実機アクセスを許可)
1llum1n4t1s Jun 22, 2026
aa20195
feat(mobile): アプリ内QRカメラスキャナで“撮って即参加”(getUserMedia + jsQR)
1llum1n4t1s Jun 22, 2026
faf4be8
feat(mobile): 無課金スタンドアローンの付箋CRUD(作成/編集/削除/グループ/ゴミ箱)
1llum1n4t1s Jun 22, 2026
88b87e1
fix(sync): WS 意図的切断時の無駄再接続 + catchup の seq 前進 + シム seed 直列化(Gemini レビ…
1llum1n4t1s Jun 23, 2026
7cb8688
fix(relay): 鍵検証前保存/クエリ署名漏れ/seq競合/SALT/Upgrade を修正(PR#8 レビュー対応)
1llum1n4t1s Jun 23, 2026
b03ece4
fix(sync): クエリ署名/復号失敗時throw/fetchタイムアウト/transportスナップショット/group共有(PR#…
1llum1n4t1s Jun 23, 2026
9d8e428
fix(mobile): scope=all/ペアリング再表示/解除時OFF/WS切替/reconcile追走/IAP本番封じ/aria/…
1llum1n4t1s Jun 23, 2026
fb47911
fix(sync): バックエンド切替時に shadow をクリアして誤削除を防ぐ(PR#8 レビュー対応)
1llum1n4t1s Jun 23, 2026
0d64086
chore(relay): 本番 go-live — custom domain + workers_dev=false + rateli…
1llum1n4t1s Jun 23, 2026
e92c177
docs(privacy): クラウド同期(E2E暗号化・ゴミ箱本文送信)をプライバシーポリシーに明記
1llum1n4t1s Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/mobile-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ぺたりん モバイル: Android ビルド(Capacitor)。
# web を Vite でビルド → cap add android(毎回生成・android/ は gitignore)→ cap sync → Gradle で debug APK。
# 署名(release)は未設定=当面 debug APK を artifact 化。release 署名は keystore を Secrets に入れてから足す。
# Actions はサプライチェーン対策で full commit SHA にピン留め(publish.yml と同方針・Dependabot で更新する運用)。
name: mobile-android

on:
workflow_dispatch: {}
push:
tags:
- "mobile-v*"

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mobile
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.0
with:
persist-credentials: false # git push しない読み取り専用ビルド=認証情報を .git/config に残さない

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
package-manager-cache: false # PR 由来の cache poisoning が release に伝播しないよう明示無効化

- name: Enable corepack (pnpm)
run: corepack enable

- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: temurin
java-version: "17"

- name: Install deps
run: pnpm install --ignore-workspace --config.dangerouslyAllowAllBuilds=true

- name: Build web (Vite)
run: pnpm build

- name: Add Android platform
run: pnpm exec cap add android

- name: Capacitor sync
run: pnpm exec cap sync android

- name: Add CAMERA permission to AndroidManifest (QR スキャナの getUserMedia 用)
working-directory: mobile/android
run: |
# 生 getUserMedia でカメラを使うため CAMERA 権限を注入する(Capacitor は自動付与しない)。
# 無いとネイティブビルドの QR スキャンが実行時にパーミッションエラーで失敗する。
manifest=app/src/main/AndroidManifest.xml
grep -q 'android.permission.CAMERA' "$manifest" || \
sed -i 's#</manifest># <uses-permission android:name="android.permission.CAMERA" />\n</manifest>#' "$manifest"
echo "--- AndroidManifest.xml の権限 ---"; grep -n 'uses-permission' "$manifest" || true

- name: Gradle assembleDebug
working-directory: mobile/android
run: ./gradlew assembleDebug --no-daemon

- name: Upload APK
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: petarin-debug-apk
path: mobile/android/app/build/outputs/apk/debug/*.apk
if-no-files-found: error
31 changes: 23 additions & 8 deletions docs/privacy-policy.en.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Petarin Privacy Policy

Last updated: 2026-06-18
Last updated: 2026-06-23

Petarin ("the Extension") respects your privacy. This policy explains what data the Extension handles.

## Information We Collect

The Extension **does not collect any personal information**, and it never transmits any data to servers operated by the developer.
The Extension **does not collect any personal information**. Except when you turn on "Cloud Sync" (described below) yourself, it never transmits any data to servers operated by the developer.

## Data Storage

Expand All @@ -16,16 +16,31 @@ The sticky notes you create (text, color, icon, placement) and your settings are
- No third party, including the developer, can access this data.
- Removing the Extension deletes the stored data.

## Multi-Device Sync (optional, off by default)
## Device Sync (optional, off by default)

The Extension includes an **optional** feature to sync your notes across multiple devices. **This feature is off by default**, and while it is off no data ever leaves your device.
The Extension and the mobile app include an **optional** feature to sync your notes across multiple devices. **This feature is off by default**, and while it is off no data ever leaves your device. There are two sync methods, and you choose which to use.

Only if you **turn it on yourself** in the "Notes Desk" settings, the notes for the domains you choose to sync are synchronized across your devices through your browser's built-in sync storage (`chrome.storage.sync`) — that is, via **the sync service provided by your browser vendor** (your Google, Microsoft, or Mozilla account, whichever you are signed into in your browser).
### A. Browser-native sync (free)

- In addition, only if you **separately turn on** "appearance settings" sync, your display preferences (which edge notes cling to, color, on-page show/hide, and note placement) are also synced across your devices through the same path. The settings that control sync itself are kept per device and are never synced.
Only if you turn sync on in the "Notes Desk" and choose "browser-native sync", the notes for the domains you choose to sync are synchronized across your devices through your browser's built-in sync storage (`chrome.storage.sync`) — that is, via **the sync service provided by your browser vendor** (your Google, Microsoft, or Mozilla account, whichever you are signed into).

- In addition, only if you **separately turn on** "appearance settings" sync, your display preferences (which edge notes cling to, color, on-page show/hide, and note placement) are also synced through the same path. The settings that control sync itself are kept per device and are never synced.
- Data handled through this path is governed by your browser vendor's privacy policy. The developer cannot access it.
- Sync only works within the same browser family (Chrome, Edge, and Firefox are separate silos; cross-browser sync is not possible).
- Turning it off stops further sync (transmission) from this device, but data you have already synced remains in the browser's sync storage so your other devices can still use it (this action does not delete it).
- Sync only works within the same browser family (Chrome, Edge, and Firefox are separate silos).

### B. Cloud sync (one-time purchase, end-to-end encrypted)

Only if you choose "Cloud Sync" and pair your devices, your notes are synced in real time across devices (including between the PC extension and the mobile app) through a relay server operated by the developer (Cloudflare Workers). This relay has the following protections:

- Note bodies, colors, icons, etc. are **encrypted on your device with AES-GCM** before being sent, and the decryption key is stored **only on your devices** (shared between devices via the QR-code/text pairing). The relay only ever receives ciphertext, and **no one — including the developer — can read the contents** (zero-knowledge).
- The domain name that identifies which site a note belongs to is also **hashed (HMAC)** on your device before sending, so the relay does not learn which sites your notes are for.
- **While sync is on, not only your active notes but also the bodies of deleted notes in the Trash are sent (also encrypted) to the relay.** The relay, lacking the decryption key, cannot read these either.
- If you lose the decryption key, the synced data cannot be recovered (the key exists only on your devices).
- Regardless of browser or mobile app, sync happens only between devices that share the same pairing.

### Common

- You can turn it off anytime in the settings. Turning it off stops further sync (transmission) from this device. Data you have already synced remains in the sync storage / relay so your other devices can still use it (this action does not delete it).

## Why Each Permission Is Used

Expand Down
27 changes: 21 additions & 6 deletions docs/privacy-policy.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# ぺたりん プライバシーポリシー

最終更新日: 2026-06-18
最終更新日: 2026-06-23

「ぺたりん」(以下「本拡張機能」)は、利用者のプライバシーを尊重します。本ポリシーは、本拡張機能が扱うデータについて説明します。

## 収集する情報

本拡張機能は、**いかなる個人情報も収集しません**。開発者が運営するサーバーへデータを送信することもありません
本拡張機能は、**いかなる個人情報も収集しません**。後述の「クラウド同期」を利用者が自分でオンにした場合を除き、開発者が運営するサーバーへデータを送信することはありません

## データの保存

Expand All @@ -16,16 +16,31 @@
- 開発者を含む第三者がこれらのデータにアクセスすることはできません。
- 本拡張機能を削除すると、保存されたデータも削除されます。

## 複数PC同期(任意機能・既定はオフ)
## 端末間の同期(任意機能・既定はオフ)

本拡張機能には、複数の端末間で付箋を同期する**任意の機能**があります。**この機能は既定でオフ**で、オフの間はデータが端末から外部へ送信されることは一切ありません。
本拡張機能・スマホアプリには、複数の端末間で付箋を同期する**任意の機能**があります。**この機能は既定でオフ**で、オフの間はデータが端末から外部へ送信されることは一切ありません。同期方式は次の 2 つがあり、利用者が選びます

利用者が「付箋デスク」の設定でこの機能を**自分でオンにした場合に限り**、同期対象として選んだドメインの付箋データが、お使いのブラウザに組み込まれた同期ストレージ(`chrome.storage.sync`)を通じて、**ブラウザのベンダーが提供する同期サービス**(Google アカウント/Microsoft アカウント/Mozilla アカウント等、利用者がブラウザにログインしているアカウント)経由で端末間に同期されます。
### A. ブラウザ標準同期(無料)

「付箋デスク」で同期をオンにし「ブラウザ標準同期」を選んだ場合に限り、同期対象として選んだドメインの付箋データが、お使いのブラウザに組み込まれた同期ストレージ(`chrome.storage.sync`)を通じて、**ブラウザのベンダーが提供する同期サービス**(Google アカウント/Microsoft アカウント/Mozilla アカウント等、利用者がブラウザにログインしているアカウント)経由で端末間に同期されます。

- さらに「見た目の設定」の同期を**任意でオンにした場合に限り**、表示に関する設定(付箋を貼る端・色味・ページ上の表示/非表示・付箋の配置位置)も同じ経路で端末間に同期されます。同期の有効/無効そのものを制御する設定は端末ごとに保持され、同期されません。
- この経路で扱われるデータの取り扱いは、各ブラウザベンダーのプライバシーポリシーに従います(開発者はこのデータにアクセスできません)。
- 同期は同一ブラウザ間でのみ機能します(Chrome/Edge/Firefox はそれぞれ独立しており、ブラウザをまたいだ同期はできません)。
- 設定でいつでもオフにできます。オフにするとこの端末からの以降の同期(送信)は停止しますが、過去に同期済みのデータは他の端末から使えるようブラウザの同期ストレージに残ります(この操作では削除しません)。

### B. クラウド同期(買い切り・エンドツーエンド暗号化)

「クラウド同期」を選んでペアリングした場合に限り、付箋データは開発者が運営する中継サーバー(Cloudflare Workers)を経由して端末間(PC の拡張機能とスマホアプリを含む)にリアルタイム同期されます。中継サーバーには次の保護があります。

- 付箋の本文・色・アイコン等は**お使いの端末で AES-GCM により暗号化**してから送信され、復号鍵は**端末内にのみ**保存されます(QR コードまたはコードのペアリングで端末間に共有します)。中継サーバーは暗号文しか受け取らず、**開発者を含む第三者は内容を読めません**(ゼロ知識)。
- どのサイトの付箋かを示すドメイン名も、端末側で**ハッシュ化(HMAC)**してから送るため、中継サーバーは「どのサイトの付箋か」も分かりません。
- **同期をオンにしている間は、現役の付箋だけでなく、ゴミ箱内の削除済み付箋の本文も(同じく暗号化して)中継サーバーへ送信されます。** 復号鍵を持たない中継サーバーは、これらの内容も読めません。
- 復号鍵を失うと同期データは復元できません(鍵は端末にのみ存在します)。
- ブラウザ/スマホアプリの種別を問わず、同じペアリングを共有した端末どうしの間でのみ同期します。

### 共通

- 設定でいつでもオフにできます。オフにするとこの端末からの以降の同期(送信)は停止します。過去に同期済みのデータは、他の端末から使えるよう同期ストレージ/中継サーバーに残ります(この操作では削除しません)。

## 権限の利用目的

Expand Down
6 changes: 6 additions & 0 deletions infra/cloudflare/relay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.wrangler/
.wrangler-state/
dist/
*.log
.dev.vars
68 changes: 68 additions & 0 deletions infra/cloudflare/relay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# petarin-relay

ぺたりんの「クラウド同期モード」用 Cloudflare Workers リレー。**任意・既定 OFF**。
拡張本体は `chrome.storage.local` が真実の源のまま。relay を使うのはクラウド同期を ON にした人だけ。

## 方式: notify-then-pull(store-and-forward)

vault(同期グループ)ごとに 1 つの `VaultDO`(Durable Object)が次を担う:

1. **暗号文ストアの窓口** — 本体は D1(`petarin-sync`)。
2. **per-vault の seq 採番** — catchup(差分取り込み)の基準。
3. **Hibernatable WebSocket の fan-out ハブ** — 変更ピンを他端末へ broadcast。

```
PC編集 → PUT /push(暗号文を D1 へ) → DO が {t:changed,d,seq} を WS broadcast
→ 他端末が GET /pull?d=… で該当ドメインだけ取得 → 端末側で復号+既存マージ
```

ferry-relay の「2 peer 生パススルー」とは別物(あちらは同時オンライン前提のファイル転送用)。

## プライバシー(E2E)

- サーバーは**暗号文しか受け取らない**。本文は端末側 `vaultKey`(AES-GCM)で暗号化済み。
- **ドメイン名も端末側で HMAC ハッシュ化**して送るため、サーバーは「どのサイトか」も知らない。
- 認証は**自己完結ペアリング鍵**: vault は ECDSA P-256 鍵ペアを持ち、QR/コードで端末間に秘密鍵を渡す。
公開鍵は初回 first-write-wins で `VaultDO` に登録。以降は**署名で検証**(秘密はサーバーに無い)。

## プロトコル

vaultId はルーティングのみに使い、`SHA-256(vaultId + SALT)` でハッシュ化してから `idFromName`(漏洩時の横入り防止)。

| 経路 | 認証の渡し方 | 内容 |
| --- | --- | --- |
| `GET /health` | なし | 疎通確認("OK") |
| `GET /sync?vault=…`(WS upgrade) | クエリ `ts/sig/pubkey` | ハイバネ WS を確立 |
| `PUT /push` | ヘッダ `X-Vault-*` | body `{d,c,n}` を D1 upsert→seq 採番→broadcast→`{seq}` |
| `GET /pull?d=…` | ヘッダ `X-Vault-*` | `{d,c,n,seq}`(無ければ 404) |
| `GET /catchup?since=…` | ヘッダ `X-Vault-*` | `{changes:[{d,seq}], seq}` |

署名対象の正規文字列(端末側と一致させる): `vaultId\nts\nmethod\npath\nsha256hex(body)`。
ヘッダ: `X-Vault-Id` / `X-Vault-Ts`(unix ms・±5分) / `X-Vault-Sig`(ECDSA P-256 SHA-256, raw r‖s, base64url) / 初回のみ `X-Vault-Pubkey`(SPKI base64url)。WS はこれらをクエリ `vault/ts/sig/pubkey` で渡す。

## セットアップ / デプロイ

```bash
pnpm -C infra/cloudflare/relay install
pnpm -C infra/cloudflare/relay typecheck # tsc --noEmit

# D1 を作成し、出力 database_id を wrangler.toml に貼る
pnpm dlx wrangler d1 create petarin-sync
pnpm -C infra/cloudflare/relay d1:migrate:local # ローカルにスキーマ適用
pnpm -C infra/cloudflare/relay dev # wrangler dev でローカル起動

# 本番化(明示 GO 後)
openssl rand -hex 32 | pnpm dlx wrangler secret put SALT
pnpm -C infra/cloudflare/relay d1:migrate:remote
pnpm -C infra/cloudflare/relay deploy # Custom Domain は wrangler.toml で確定してから
```

Workers Paid($5/月・Ferry / RealTimeTranslator と共有)必須(Durable Objects のため)。
個人規模なら従量はほぼ枠内(アイドルはハイバネで duration 課金 0)。

## まだ無いもの(B の続き)

- 客側 `RelayTransport`(拡張の `setSyncTransport` に差す。HTTP + E2E 暗復号 + ドメイン HMAC)。
- ペアリング UI(QR 生成/読取 + コード貼り付け)と manage の同期パネルのモード選択(排他 3 モード)。
- background.js の WS 接続保持・変更ピン受信→該当ドメインだけ reconcile・前面復帰 catchup。
- モバイル background 時の即時性(catchup のみ / Web Push 併用)はスマホアプリ設計後に決定。
17 changes: 17 additions & 0 deletions infra/cloudflare/relay/migrations/0001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- ぺたりん 同期リレーの暗号文ストア(D1: petarin-sync)。
-- サーバーは暗号文しか持たない: ciphertext/nonce は端末側 vaultKey で AES-GCM 暗号化済み、
-- domain_hash は端末側で HMAC ハッシュ化済み(サーバーは「どのサイトか」も知らない)。
-- vault_id はハッシュ化済み DO id 文字列(生 vaultId は保存しない)。

CREATE TABLE IF NOT EXISTS notes (
vault_id TEXT NOT NULL, -- ハッシュ化済み vault 識別子(= DO id 文字列)
domain_hash TEXT NOT NULL, -- HMAC(vaultKey, domain) の hex
ciphertext TEXT NOT NULL, -- AES-GCM 暗号文(base64url)
nonce TEXT NOT NULL, -- AES-GCM nonce/IV(base64url)
seq INTEGER NOT NULL, -- per-vault 単調増加(catchup の since 比較に使う)
updated_at INTEGER NOT NULL, -- サーバー受領時刻(ms)
PRIMARY KEY (vault_id, domain_hash)
);

-- catchup(seq > since)と per-vault 走査用。
CREATE INDEX IF NOT EXISTS idx_notes_vault_seq ON notes (vault_id, seq);
17 changes: 17 additions & 0 deletions infra/cloudflare/relay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "petarin-relay",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"d1:migrate:local": "wrangler d1 migrations apply petarin-sync --local",
"d1:migrate:remote": "wrangler d1 migrations apply petarin-sync --remote"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260526.1",
"typescript": "^5.6.0",
"wrangler": "^4.94.0"
}
}
Loading