Skip to content

DM APIとWebSocket整理#612

Open
tako0614 wants to merge 1 commit intomasterfrom
codex/remove-e2ee-routes-and-add-dm-endpoint
Open

DM APIとWebSocket整理#612
tako0614 wants to merge 1 commit intomasterfrom
codex/remove-e2ee-routes-and-add-dm-endpoint

Conversation

@tako0614
Copy link
Copy Markdown
Owner

@tako0614 tako0614 commented Aug 22, 2025

概要

  • フロントエンドからMLS依存コードを削除し、DMを平文送受信に簡素化
  • 設定画面やプロフィールなど関連コンポーネントをMLS不要な構成へ更新
  • microblogのユーザー情報取得をメモリキャッシュのみに変更
  • 友だちごとのDMスレッドを1つに統合し、ルームID依存を解消
  • ts-mls関連の依存を設定・ロックファイルから除去し、不要なハンドシェイク処理を削除
  • ドキュメントからMLS/E2EEの説明を削除し、新しいDM仕様を追記
  • アカウント登録後はMLS鍵生成を行わず、DMの初回同期のみ実行

テスト

  • deno fmt app/takos_host/client/src/pages/WelcomePage.tsx
  • deno lint app/takos_host/client/src/pages/WelcomePage.tsx

https://chatgpt.com/codex/tasks/task_e_68a8ed9b4f2c8328ae8427aebd9f891d

Summary by CodeRabbit

  • 新機能
    • 1対1のダイレクトメッセージ(DM)を追加(RESTで作成/取得、ActivityPubのCreate/Noteで送信)。送受信者へリアルタイム通知。
  • ドキュメント
    • READMEほかをDM中心に更新。暗号化/MLS説明を削除。
    • 「ファイルアップロード設定」を追加(サイズ上限・MIME/拡張子の許可/拒否の環境変数)。
  • リファクタ
    • 旧E2EE/MLSとチャットルーム機能を廃止し、DM中心に簡素化。WebSocket処理とクライアントの設定/チャットUI(鍵管理・友だち部屋等)を整理。
  • テスト
    • 旧MLS関連テストを削除。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 22, 2025

Walkthrough

DM機能を新設し、MLS/E2EE・チャットルーム・鍵管理関連コードとドキュメントを全面的に削除/置換。RESTとActivityPub Create/Note単一宛先でのDM保存/通知を実装。DBインターフェースとモデルをDM向けに再構成。WebSocketのバイナリ/MLS経路を整理。クライアント側MLS依存UI/ストレージ/テストを撤去。

Changes

Cohort / File(s) Summary
Routing/Server
app/api/server.ts, app/api/routes/dm.ts, app/api/routes/ws.ts, app/api/routes/activitypub.ts
e2eeルートをDMルートへ置換。/dm POST/GETを追加(認証・検証・DB保存・WS通知)。WSのバイナリ/MLSハンドラ削除しJSON型で統一。ActivityPub Createの単一宛先NoteをDMとして保存・通知・必要に応じてリモート配達。
DB API/Impl
app/shared/db.ts, app/api/DB/mod.ts, app/api/DB/mongo.ts
DBインターフェースからチャットルーム/鍵/暗号/握手系を削除。DM保存/相互取得APIを追加。startKeyPackageCleanupJobの公開停止。Mongo実装にDMMessage対応のsaveDMMessage/listDMsBetween追加。
Models (DM 追加)
app/api/models/takos/dm_message.ts
DMMessageモデル/スキーマ新設(from/to/content/createdAt、tenantScope適用)。
Models (MLS 削除)
app/api/models/takos/encrypted_message.ts, .../handshake_message.ts, .../key_package.ts, app/api/models/takos_host/*
暗号メッセージ/握手/キーパッケージ関連モデル(テナント/ホスト両方)を削除。
Routes (MLS 削除)
app/api/routes/e2ee.ts, app/api/routes/e2ee_test.ts
E2EEエンドポイントと関連テストを全削除。
ActivityPub Actor 生成
app/api/utils/activitypub.ts, app/api/routes/activitypub.ts
俳優の@contextからMLS拡張URLとkeyPackagesを除去。ユーザーエンドポイントのkeyPackages公開処理を削除。
Client: アプリ/設定/プロフィール/チャットUI
app/client/src/components/Application.tsx, .../Setting/index.tsx, .../Profile.tsx, .../chat/ChatRoomList.tsx, .../chat/ChatSettingsOverlay.tsx, .../chat/ChatTitleBar.tsx, .../chat/FriendList.tsx, .../chat/FriendRoomList.tsx
MLS連携UI・バルクトップアップ・鍵管理UI/状態を削除。チャット設定/タイトルバーのMLS表示を除去。FriendRoomList等の専用UIを整理/削除し簡素化。初回チャット開始の部屋作成呼び出しを削除。
Client: E2EE 層削除
app/client/src/components/e2ee/*
api.ts, binding.ts, mls_message.ts, mls_wrapper.ts, storage.ts, useMLS.ts, mls_test.ts を削除(MLS API/検証/記録/ストレージ/テスト一式)。
Client: 設定/依存
app/client/deno.json, app/client/src/utils/config.ts
ts-mls依存をimportsから削除。KPプール設定APIを削除。
Microblog キャッシュ
app/client/src/components/microblog/api.ts
ユーザー情報キャッシュをメモリ内に簡略化。関数シグネチャからaccountId/非同期化を削除・同期キャッシュ化。
Host クライアント/ページ
app/takos_host/client/src/api.ts, .../pages/WelcomePage.tsx, .../state.ts
MLS握手送信API削除。WelcomePageでログイン時に自身間のDM履歴取得を起動。stateにrootDomainState/userNameStateを追加。
Docs
README.md, docs/chat_ux.md, docs/key-sharing.md, docs/multi-device-sync.md
Chat API/MLS説明をDM APIとファイルアップロード設定に置換。E2EE/鍵共有/多端末同期のMLS前提説明をDM前提に全面改稿・簡素化。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as API (/api/dm)
  participant DB
  participant WS as WebSocket Hub
  participant UserA as User A
  participant UserB as User B

  Client->>API: POST /api/dm { from, to, content }
  API->>DB: saveDMMessage(from, to, content)
  DB-->>API: { id, from, to, content, createdAt }
  API->>WS: sendToUser(from, { type:"dm", payload })
  API->>WS: sendToUser(to, { type:"dm", payload })
  API-->>Client: 200 { id, from, to, content }

  note over WS,UserA: 双方へDM通知
  note over WS,UserB: 双方へDM通知
Loading
sequenceDiagram
  autonumber
  participant Inbox as ActivityPub Inbox
  participant Handler as activity_handlers(Create)
  participant DB
  participant Deliver as deliverActivityPubObject

  Inbox->>Handler: Create{ object: Note, to:[target] }
  alt toが単一Actorで非公開コレクション
    Handler->>DB: saveMessage(..., {to, cc})
    Handler-->>Inbox: 202 (dm handled)
    opt リモート宛先
      Handler->>Deliver: POST Note to remote inbox
      Deliver-->>Handler: 2xx/4xx
    end
  else それ以外
    Handler->>DB: saveObject(...)
    Handler-->>Inbox: 202 (post handled)
  end

  note over Handler: 単一宛先NoteをDMとして分岐
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Poem

ぴょんと跳ねても 影は一つ
ひそやかDM、月の裏口
鍵は要らない 今日のことば
ふたりの宙へ 即席ショートカット
カサコソ通知で こんばんは 📨🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/remove-e2ee-routes-and-add-dm-endpoint

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/api/routes/ws.ts (1)

60-70: 任意ユーザーへのなりすましが可能(重大)— register の payload.user を無検証で採用

register メッセージ受信時に payload.user をそのまま state.user として登録しています。セッション検証も署名検証もなく、攻撃者が任意のユーザー名で登録し、そのユーザー宛の DM 通知を受け取れる恐れがあります。

是正案(推奨順):

  • A) サーバー側で WS 接続時に認証済みユーザーを確定し、自動登録する(クライアントからの register を廃止/任意化)。c.get("user")(認証ミドルウェアで設定)や署名付きクッキー/JWT から復元してください。
  • B) 互換性のために register を残す場合でも、payload.user がサーバーで認証済みのユーザーと一致することを必須チェックにしてください。一致しない場合はエラーを返し、ソケットをクローズ。

参考差分(案 A:onOpen で登録、register 削除):

-import type { WSContext } from "hono/ws";
+import type { WSContext } from "hono/ws";
+import type { Context } from "hono";
 
-export type WsState = Record<string, unknown>;
+export interface WsState {
+  context: Context;
+  user?: string;
+}
@@
-registerMessageHandler("register", (payload, ws, state) => {
-  const user = (payload as { user?: string }).user;
-  if (!user) return;
-  state.user = user;
-  let set = userSockets.get(user);
-  if (!set) {
-    set = new Set();
-    userSockets.set(user, set);
-  }
-  set.add(ws);
-});
+// register に頼らず、onOpen でサーバーが判定したユーザーを登録する方針に変更
@@
   upgradeWebSocket((c) => {
-    const state: WsState = { context: c };
+    const state: WsState = { context: c };
     return {
-      onOpen(_evt, ws) {
-        for (const h of openHandlers) h(ws, state);
-      },
+      onOpen(_evt, ws) {
+        // 例:認証ミドルウェアでセット済みのユーザーを取得(実装に合わせて調整)
+        const authedUser = (c.get("user") as string | undefined);
+        if (!authedUser) {
+          // 未認証なら接続を終了(適切なコード/理由に置き換え)
+          try { ws.close(4401, "unauthorized"); } catch {}
+          return;
+        }
+        state.user = authedUser;
+        let set = userSockets.get(authedUser);
+        if (!set) {
+          set = new Set();
+          userSockets.set(authedUser, set);
+        }
+        set.add(ws);
+        for (const h of openHandlers) h(ws, state);
+      },

互換性案(B:register を残すが一致検証を追加)も提示可能です。必要なら提示します。

app/api/activity_handlers.ts (1)

114-117: 環境変数の取得は getEnv(c) に統一(ガイドライン準拠)

(c as { get("env") }) はガイドライン違反です。getEnv(c) に寄せてください。

提案差分:

-import {
-  createAcceptActivity,
-  deliverActivityPubObject,
-  extractAttachments,
-  getDomain,
-} from "./utils/activitypub.ts";
+import {
+  createAcceptActivity,
+  deliverActivityPubObject,
+  extractAttachments,
+  getDomain,
+  getEnv,
+} from "./utils/activitypub.ts";
@@
-      const env = (c as { get: (k: string) => unknown }).get("env") as Record<
-        string,
-        string
-      >;
+      const env = getEnv(c as Context);
🧹 Nitpick comments (27)
app/client/src/components/chat/FriendList.tsx (1)

232-239: アクセシビリティ強化(div+onClick はキーボード操作不可)

div に onClick だけだとキーボード操作で選択できません。role="button"tabIndex、Enter/Space での起動を追加すると操作性が上がります。

適用例(対象行に追加):

-              <div
+              <div
                 class={`flex items-center p-3 rounded-lg cursor-pointer transition-colors ${
                   props.selectedFriend === friend.id
                     ? "bg-[#4a4a4a]"
                     : "hover:bg-[#3c3c3c]"
                 }`}
-                onClick={() => props.onSelectFriend(friend.id)}
+                role="button"
+                tabIndex={0}
+                onClick={() => props.onSelectFriend(friend.id)}
+                onKeyDown={(e) => {
+                  if (e.key === "Enter" || e.key === " ") {
+                    e.preventDefault();
+                    props.onSelectFriend(friend.id);
+                  }
+                }}
               >

補足: 本ファイルの normalizeHandle と Profile.tsx の normalizeActor は目的が近いので utils へ集約すると DRY になります。

app/client/src/components/Profile.tsx (1)

242-249: ハンドル正規化の重複を共通化

本ファイルの normalizeActor と FriendList.tsx の normalizeHandle が重複しています。utils/handle.ts のような共通ユーティリティへ切り出すと保守性が上がります(片側の修正漏れ防止)。

適用例(外部ファイル追加のため参考コード):

// utils/handle.ts
export function normalizeHandleOrActor(id: string): string | undefined {
  if (!id) return;
  if (id.startsWith("http")) {
    try {
      const u = new URL(id);
      const name = u.pathname.split("/").pop() || "";
      return name ? `${name}@${u.hostname}` : undefined;
    } catch {
      return;
    }
  }
  return id.includes("@") ? id : undefined;
}
app/takos_host/client/src/pages/WelcomePage.tsx (1)

270-285: 初期 DM 同期フェッチの堅牢化提案

以下の改善を任意でご検討ください。

  • 空値ガード
    • userName() または rootDomain() が空文字の場合は早期リターンし、不正な URL を生成しないようにします。
  • 認証情報の送信
    • Cookie ベースの認証を利用している場合、fetch のオプションに { credentials: "include" } を追加すると確実です。
  • レスポンスチェック&ロギング
    • res.ok を確認し、エラー時にはステータスや例外情報をログ出力するとトラブルシュートが容易になります。
  • サーバ側挙動の確認済み
    • app/api/routes/dm.ts ではクエリパラメータに対し文字列型制約のみがあり、user1 === user2 のケースもそのまま受け入れ、DB の listDMsBetween では同一ユーザー間のメッセージを検索する設計になっています。仕様変更は不要です。

適用例(抜粋):

   onMount(async () => {
-    if (!loggedIn()) return;
-    const actor = `${userName()}@${rootDomain()}`;
+    if (!loggedIn()) return;
+    const name = userName();
+    const domain = rootDomain();
+    if (!name || !domain) return; // 空値ガード
+    const actor = `${name}@${domain}`;
     try {
-      await fetch(
-        `/api/dm?user1=${encodeURIComponent(actor)}&user2=${encodeURIComponent(actor)}`,
-      );
+      const url = `/api/dm?user1=${encodeURIComponent(actor)}&user2=${encodeURIComponent(actor)}`;
+      const res = await fetch(url, { credentials: "include" });
+      if (!res.ok) {
+        console.debug("Initial DM sync failed:", res.status);
+      }
     } catch (e) {
-      // 失敗しても無視
+      console.debug("Initial DM sync error:", e);
     }
   });
app/api/models/takos/dm_message.ts (1)

4-15: 会話キーの導入で OR クエリを簡素化(設計オプション)

A↔B の双方向検索を $or: [{from:A,to:B},{from:B,to:A}] で行うより、conversationKey = sort([from,to]).join('#') のような正規化キーを持たせ、{tenant_id, conversationKey, createdAt} で単一等価検索+ソートにするとインデックス効率と実装がシンプルになります。

参考(概念実装):

dmMessageSchema.add({
  conversationKey: { type: String, index: true },
});

dmMessageSchema.pre("validate", function (next) {
  const a = String(this.from || "").trim().toLowerCase();
  const b = String(this.to || "").trim().toLowerCase();
  this.conversationKey = [a, b].sort().join("#");
  next();
});

// インデックス
dmMessageSchema.index({ tenant_id: 1, conversationKey: 1, createdAt: -1 });
app/client/src/components/Application.tsx (1)

22-33: モバイル判定は matchMedia の利用が簡潔で効率的

resize ハンドラで幅判定していますが、matchMedia("(max-width: 768px)")change を使うと負荷低減・判定の単純化ができます。現状でも問題は軽微ですが、将来的な保守性向上に有効です。

参考差分(概念):

-  onMount(() => {
-    const checkMobile = () => {
-      setIsMobile(globalThis.innerWidth <= 768);
-    };
-    checkMobile();
-    globalThis.addEventListener("resize", checkMobile);
-    connectWebSocket();
-    return () => globalThis.removeEventListener("resize", checkMobile);
-  });
+  onMount(() => {
+    const mq = globalThis.matchMedia?.("(max-width: 768px)");
+    const update = () => setIsMobile(mq ? mq.matches : globalThis.innerWidth <= 768);
+    update();
+    mq?.addEventListener?.("change", update);
+    connectWebSocket();
+    return () => mq?.removeEventListener?.("change", update);
+  });
app/client/src/components/chat/ChatSettingsOverlay.tsx (1)

13-23: モーダルのアクセシビリティ強化(role/aria/Escape)

最小 UI で良いのですが、role="dialog"aria-modal="true"、タイトル参照、Escape で閉じる対応があると支援技術・キーボード操作に優しくなります。

適用差分案:

-      <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-        <div class="bg-white rounded p-4 w-72 text-black">
-          <h2 class="text-lg font-bold mb-2">チャット設定</h2>
+      <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
+        <div
+          class="bg-white rounded p-4 w-72 text-black outline-none"
+          role="dialog"
+          aria-modal="true"
+          aria-labelledby="chat-settings-title"
+          tabIndex={-1}
+          onKeyDown={(e) => { if (e.key === "Escape") props.onClose(); }}
+        >
+          <h2 id="chat-settings-title" class="text-lg font-bold mb-2">チャット設定</h2>
           <p class="mb-4">現在、設定項目はありません。</p>
           <button
             type="button"
             class="px-4 py-2 rounded bg-gray-700 text-white"
             onClick={props.onClose}
           >
             閉じる
           </button>
         </div>
       </div>
app/client/src/components/Setting/index.tsx (1)

16-24: ログアウトの多重実行を防止し、ボタンを無効化する

現在は連打で複数回リクエストされ得ます。UX とサーバー負荷の観点から、処理中はボタンを無効化してください。

実装例(コンポーネント内追記):

import { createSignal } from "solid-js";

export function Setting() {
  const [isLoggingOut, setIsLoggingOut] = createSignal(false);

  const handleLogout = async () => {
    if (isLoggingOut()) return;
    setIsLoggingOut(true);
    try {
      await apiFetch("/api/logout", { method: "POST" });
    } catch (err) {
      console.error("logout failed", err);
    } finally {
      setIsLoggedIn(false);
      setIsLoggingOut(false);
    }
  };

  // ...
  <button
    type="button"
    class="bg-red-700 text-white px-4 py-2 rounded hover:bg-red-800 transition disabled:opacity-60"
    disabled={isLoggingOut()}
    onClick={handleLogout}
  >
    ログアウト
  </button>
}
app/api/routes/ws.ts (2)

91-104: onMessage の入力検証と未知 type のエラーレスポンスを追加

handler?.(msg.payload, ...) で未定義 type を黙殺しています。クライアント不具合の早期検知のため、type の存在チェックと未知 type のエラーレスポンスを返すのが安全です。また、ハンドラ実行時の例外を捕捉して onError に委譲すると運用性が上がります。

適用差分案:

       if (typeof evt.data === "string") {
         try {
           const msg = JSON.parse(evt.data);
-          const handler = messageHandlers.get(msg.type);
-          handler?.(msg.payload, ws, state);
+          if (!msg || typeof msg.type !== "string") {
+            ws.send(JSON.stringify({ error: "missing_type" }));
+            return;
+          }
+          const handler = messageHandlers.get(msg.type);
+          if (!handler) {
+            ws.send(JSON.stringify({ error: "unknown_type", type: msg.type }));
+            return;
+          }
+          Promise.resolve(handler(msg.payload, ws, state)).catch((_e) => {
+            ws.send(JSON.stringify({ error: "handler_error", type: msg.type }));
+          });
         } catch {
           ws.send(JSON.stringify({ error: "invalid message" }));
         }
       } else {
         ws.send(JSON.stringify({ error: "binary_payload_not_allowed" }));
       }

23-39: send/broadcast 時の CLOSE 状態送信を防止(微最適化)

閉じたソケットに対する send は例外やノイズログの原因になります。readyState === WebSocket.OPEN を確認してください。

適用差分案:

 export function sendToUser(user: string, data: unknown) {
   const sockets = userSockets.get(user);
   if (!sockets) return;
   const message = typeof data === "string" ? data : JSON.stringify(data);
   for (const s of sockets) {
-    s.send(message);
+    // OPEN のみ送信
+    if ((s.raw as WebSocket).readyState === WebSocket.OPEN) s.send(message);
   }
 }
@@
   const message = typeof data === "string" ? data : JSON.stringify(data);
   for (const set of userSockets.values()) {
     for (const s of set) {
-      s.send(message);
+      if ((s.raw as WebSocket).readyState === WebSocket.OPEN) s.send(message);
     }
   }
 }
app/client/src/components/chat/ChatRoomList.tsx (2)

147-162: normalizeHandle の重複を共通化してバグ予防

本ファイルと FriendList.tsx にほぼ同等の normalizeHandle 実装が重複しています。将来の仕様変更時に片方の修正漏れが起きやすいです。utils/handle.ts 等に切り出して単一実装へ寄せてください。

共通化イメージ:

// app/client/src/utils/handle.ts
export function normalizeHandle(id?: string): string | undefined {
  if (!id) return undefined;
  if (id.startsWith("http")) {
    try {
      const u = new URL(id);
      const name = u.pathname.split("/").pop() || "";
      if (!name) return undefined;
      return `${name}@${u.hostname}`;
    } catch {
      return undefined;
    }
  }
  return id.includes("@") ? id : undefined;
}

使用側:

-import { getDomain } from "../../utils/config.ts";
+import { getDomain } from "../../utils/config.ts";
+import { normalizeHandle } from "../../utils/handle.ts";

399-405: selectedRoomState は一貫して Room.id を保持しています

下記のとおり、selectedRoomState から ChatRoomListFriendList まで渡される値はすべて Room.id(DM では友だちハンドル、グループではチャンネルID)として統一されており、型・値の不整合はありません。

  • selectedRoomState(atom<string | null>)は常に Room.id を保持しています
  • Chat.tsx で取得した selectedRoom() の値をそのまま ChatRoomListFriendListselectedFriendonSelectFriend)に渡しています
  • DM API 呼び出し時の /api/dmto フィールドにも同じ selectedRoom() の値が使われており、正しく送信されています

よって、旧実装との互換性や選択値の意味に問題はありません。

オプション:更なる可読性向上のため、命名を明確化するリファクタを検討してもよいでしょう

  • ChatRoomListProps.onSelectonSelectFriend
  • FriendListProps.selectedFriendselectedRoomId / selectedFriendId
app/client/src/components/microblog/api.ts (3)

355-358: 同期API化に伴うawaitの取り残しを削除してください

getCachedUserInfo/setCachedUserInfo は同期関数ですが、呼び出し元に await が残っています。意味上は問題ないものの、読みやすさと意図の明確化のために除去を推奨します。Promise.all に同期処理を渡している箇所も合わせて整理してください。

適用例:

-    const cached = await getCachedUserInfo(identifier);
+    const cached = getCachedUserInfo(identifier);
...
-    await setCachedUserInfo(identifier, userInfo);
+    setCachedUserInfo(identifier, userInfo);
...
-      const cachedInfo = await getCachedUserInfo(identifier);
+      const cachedInfo = getCachedUserInfo(identifier);

Also applies to: 370-372, 388-394


312-319: UserInfo 型定義の重複を解消し、単一起点へ集約を

app/api/services/user-info.ts に同等の UserInfo が存在します。型の二重管理は将来的な乖離や不整合の原因になります。shared 層など参照可能な共通モジュールへ移動し、このファイルからはそれをimportする形へ寄せてください。


321-328: メモリキャッシュに上限とエビクション戦略を追加検討

時間判定は読み出し時のみで、Mapからの掃除が行われません。長期稼働で識別子が増え続けるとメモリリーク様の増加が起こり得ます。サイズ上限(例: LRU 1000件)と定期的なエビクション/弱参照の導入を検討してください。

docs/key-sharing.md (1)

3-5: 平文DMへの移行に伴うリスク表示と仕様例(JSON-LD)の明記を追加してください

  • 「サーバ管理者/侵害者からも内容が読める」「転送はTLSだがE2EEではない」等のリスクを注意書きとして明示してください。
  • ActivityPub互換実装として、Create/Note の最小完全例(@context/to/cc/可視性)を載せると実装者/運用者に親切です。

例(参考・最小例):

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": "https://example.com/users/alice",
  "to": ["https://remote.example/users/bob"],
  "cc": [],
  "object": {
    "type": "Note",
    "attributedTo": "https://example.com/users/alice",
    "to": ["https://remote.example/users/bob"],
    "cc": [],
    "content": "hi bob (plaintext DM)",
    "sensitive": false
  }
}

加えて、配送ポリシー(例: フォロワーコレクションや Public への拡散と無関係であること、サーバ間の保存/ログ方針)にも一言触れておくと誤解が減ります。

docs/chat_ux.md (1)

15-18: to が1件=DM扱い」の前提条件と相互運用注意点を補足してください

AP実装間の相互運用では、可視性(to/cc/audience/to: Publicでないこと)や配信スコープの解釈が実装依存な側面があります。本仕様では「アプリ層でDMとして扱う」という前提を明言し、以下を補足すると実装者に親切です。

  • to は受信者1件、cc は空、Public へは送らない(非公開)。
  • 受信側がルーム自動作成する条件(例: 自身宛の非公開NoteをDM扱いにする)。
  • 既存ルームとの突合ルール(同一相手との1:1は同じルームにスレッド化等)。
  • 失敗時(ブロック、配信拒否)のUI出し分け。
  • JSON-LD最小例を「仕様」節か付録にリンク。
app/api/routes/dm.ts (2)

31-33: as { _id: string } のキャスト依存を解消し、DBインターフェイスへ型を上げてください

saveDMMessage の戻り値を共有インターフェイス(app/shared/db.ts)で型定義し、ここではその型を受ける形にしてください。キャストは将来の変更で静的検査が効かず破損しやすいです。


21-23: 入力サイズの上限・内容検証を追加検討

DoSや肥大化を避けるため content の最大長(例: 2000)や禁止内容(空文字のみ、巨大な連続空白など)をZodで制限しておくと事故を減らせます。

README.md (2)

177-182: RESTエンドポイントの認証・応答スキーマ・ページング仕様を明文化

実装者/クライアント双方の合意形成のため、要ログイン/レート制限、レスポンス形式、ソート順、ページング/カーソル等を記述してください。

提案差分:

 ### RESTエンドポイント

-- `POST /api/dm` – `{ "from": "alice", "to": "bob", "content": "こんにちは" }`
+- `POST /api/dm` (要ログイン, RateLimit適用) – `{ "from": "alice", "to": "bob", "content": "こんにちは" }`
   を送信してDMを作成
-- `GET /api/dm?user1=alice&user2=bob` – 2人間のメッセージ一覧を取得
+- `GET /api/dm?user1=alice&user2=bob&limit=50&before=ISO8601` (要ログイン, RateLimit適用) – 2人間のメッセージ一覧を取得(既定は新しい順、`before` でページング)
+
+レスポンス例:
+```json
+[
+  { "id": "dm_...", "from": "alice@example.com", "to": "bob@example.com", "content": "こんにちは", "createdAt": "2025-08-22T12:34:56.000Z" }
+]
+```
+
+WebSocket通知:
+```json
+{ "type": "dm", "payload": { "id": "dm_...", "from": "alice@example.com", "to": "bob@example.com", "content": "こんにちは", "createdAt": "2025-08-22T12:34:56.000Z" } }
+```

183-199: ActivityPubでDM判定される条件を厳密化して明記

to が単一Actorでも cc/bto/bcc に Public や followers が混在するとDMになりません。判定条件と落とし穴の注意を追記してください。

提案差分:

 ### ActivityPub経由

-`Create` アクティビティで `Note` を送信する際、`to` 配列に受信者の
-アクターURIを1件だけ指定するとDMとして扱われます。
+`Create` アクティビティで `Note` を送信する際、`to` 配列に受信者の
+アクターURIを1件だけ指定し、かつ `cc`/`bto`/`bcc` に Public や followers 等のコレクションを含めない場合にDMとして扱われます。
+(Public, followers, outbox, collections, liked/likes などが含まれると公開投稿として処理されます)
docs/multi-device-sync.md (1)

3-9: 同期仕様の詳細(順序、重複排除、再取得、通知ペイロード)を補足

実装/クライアント間の取り決めとして、メッセージの並び順、欠落時の再同期方法、WS通知の形式と重複排除戦略を追記してください。

追記案:

  • 並び順: createdAt 降順(または昇順)で返す。API/WSとも同一フィールド名。
  • 再同期: GET /api/dm?user1&user2&before=ISO8601&limit=N でページング。クライアントは最後に取得した最古の createdAtbefore に指定。
  • 重複排除: id をキーに冪等にマージする(WSとAPI両経路で同一IDが来る可能性)。
  • WS通知ペイロード例:
    { "type": "dm", "payload": { "id": "...", "from": "alice@example.com", "to": "bob@example.com", "content": "...", "createdAt": "..." } }
app/api/activity_handlers.ts (3)

92-110: DM判定をより厳密に(cc/bto/bcc を除外し、コレクション宛先を排除)

to が1件という条件に加えて、cc/bto/bcc に Public や followers 等が混在するとDMにすべきでないため、明示的に空 or 非コレクションであることを確認してください。

提案差分:

-    // to が1件の Note は DM とみなす
-    if (objTypes.includes("Note") && toList.length === 1) {
+    // to が1件 かつ cc/bto/bcc が空 or 非コレクション の Note は DM とみなす
+    if (objTypes.includes("Note") && toList.length === 1) {
       const target = toList[0];
       const isCollection = (url: string): boolean => {
         if (url === "https://www.w3.org/ns/activitystreams#Public") return true;
         try {
           const path = new URL(url).pathname;
           return path.endsWith("/followers") ||
             path.endsWith("/following") ||
             path.endsWith("/outbox") ||
             path.endsWith("/collections") ||
             path.endsWith("/liked") ||
             path.endsWith("/likes");
         } catch {
           return false;
         }
       };
       if (isCollection(target)) return;
+      const ccList = Array.isArray(obj.cc) ? obj.cc : [];
+      const btoList = Array.isArray((obj as any).bto) ? (obj as any).bto : [];
+      const bccList = Array.isArray((obj as any).bcc) ? (obj as any).bcc : [];
+      if ([...ccList, ...btoList, ...bccList].some((x) => isCollection(String(x)))) {
+        return;
+      }

130-140: WS通知ペイロードに createdAt を含めてクライアントの整列/重複排除を容易に

レスポンス/通知のスキーマを揃えるとクライアント実装が単純になります。

提案差分:

-      const payload = {
+      const payload = {
         id: String(msg._id),
         from: fromHandle,
         to: toHandle,
         content: typeof obj.content === "string" ? obj.content : "",
+        createdAt:
+          (msg as any).published?.toISOString?.() ??
+          (msg as any).createdAt?.toISOString?.() ??
+          new Date().toISOString(),
       };

119-129: DM保存経路の統一と添付情報抽出の追加を検討してください

ActivityPub経由のDMでは saveMessage、REST API経由のDMでは saveDMMessage を使っており、別々の永続化先に保存されるため、UIや履歴が分断されてしまいます。いずれか一方に統一するか、ビュー層で両者を統合する仕組みを導入してください。また、Note オブジェクトに含まれる attachment(s) もDM保存時に抽出・保存できるように実装を追加することをおすすめします。

• 主な関連箇所

  • app/api/activity_handlers.ts(saveMessage を利用)
  • app/shared/db.ts(saveDMMessage/listDMsBetween 宣言)
  • app/api/DB/mongo.ts(saveDMMessage/listDMsBetween 実装)
  • app/api/routes/dm.ts(REST DM API ルート)

• 添付情報抽出を追加する例(保存先統一は方針に応じて調整)

       const db = createDB(env);
+      const attachments = extractAttachments(obj);
+      const extra: Record<string, unknown> =
+        typeof obj.extra === "object" && obj.extra !== null
+          ? { ...(obj.extra as Record<string, unknown>) }
+          : {};
+      if (attachments.length > 0) extra.attachments = attachments;
       const msg = await db.saveMessage(
         domain,
         actor,
         typeof obj.content === "string" ? obj.content : "",
-        typeof obj.extra === "object" && obj.extra !== null
-          ? obj.extra as Record<string, unknown>
-          : {},
+        extra,
         { to: toList, cc: Array.isArray(obj.cc) ? obj.cc : [] },
       ) as { _id: unknown };
app/shared/db.ts (1)

69-71: DBインターフェースの定義確認

以下の出力で app/shared/db.tsDB インターフェースが確認されました。saveDMMessagelistDMsBetween が共に Promise<unknown> を返す仕様になっており、型安全性が担保されていません。

export interface DB {
  // ...
  saveDMMessage(from: string, to: string, content: string): Promise<unknown>;
  listDMsBetween(user1: string, user2: string): Promise<unknown[]>;
  // ...
}

指摘内容

  • 戻り値が unknown のため、呼び出し元での型チェックが困難
  • DMMessage 型({ id: string; from: string; to: string; content: string; createdAt: Date })を導入し、メソッドシグネチャを Promise<DMMessage> / Promise<DMMessage[]> に変更
  • ユーザー識別子は username@domain 形式に正規化する旨をコード上に明示

対応案(例)

+export interface DMMessage {
+  id: string;
+  from: string;      // username@domain
+  to: string;        // username@domain
+  content: string;
+  createdAt: Date;
+}
 export interface DB {
@@
-  saveDMMessage(from: string, to: string, content: string): Promise<unknown>;
+  saveDMMessage(
+    from: string /* username@domain */,
+    to: string /* username@domain */,
+    content: string
+  ): Promise<DMMessage>;
@@
-  listDMsBetween(user1: string, user2: string): Promise<unknown[]>;
+  listDMsBetween(
+    user1: string /* username@domain */,
+    user2: string /* username@domain */,
+    opts?: { limit?: number; before?: Date }
+  ): Promise<DMMessage[]>;

実装上のポイント

  • app/api/models/takos/dm_message.ts ですでに Mongoose モデル DMMessage が定義されているので、shared 側に同名の型を定義し、メソッド実装部では Mongoose ドキュメントをこの型にマッピング
  • from/to の引数前処理で、たとえば normalizeUsername(raw: string): string を呼んで username@domain 形式を保証
app/api/DB/mongo.ts (2)

360-368: $or を避けるためにペア正規化キー(pairKey)を保存し、クエリ効率を改善

from/to の順序差分を吸収した pairKey(例: min(from,to)|max(from,to))を保存しておくと、クエリ側で $or を使わず単一条件でヒットさせられます。併せて (tenant_id, pairKey, createdAt) の複合インデックスを張ることで大量会話でも安定した性能が出ます。

以下の差分で pairKey を保存できます(最小変更):

-  async saveDMMessage(from: string, to: string, content: string) {
-    const doc = new DMMessage({ from, to, content });
+  async saveDMMessage(from: string, to: string, content: string) {
+    const pairKey = from < to ? `${from}|${to}` : `${to}|${from}`;
+    const doc = new DMMessage({ from, to, content, pairKey });
     if (this.env["DB_MODE"] === "host") {
       (doc as unknown as { $locals?: { env?: Record<string, string> } })
         .$locals = { env: this.env };
     }
     await doc.save();
     return doc.toObject();
   }

補助: モデル側の推奨インデックス(変更は別ファイル)

// app/api/models/takos/dm_message.ts(例)
schema.index({ tenant_id: 1, pairKey: 1, createdAt: 1 });

必要なら dm_message.ts 用の完全なパッチを作ります。


370-380: listDMsBetween のクエリ最適化とページング上限の追加を検討

現状 $or で全件を昇順取得するため、会話履歴が多いとI/O/メモリ負荷が高くなります。上記の pairKey を用いた単一条件化に加え、API/UXの要件に応じてページング(cursor/before/since/limit)を導入するのが安全です。少なくともハード上限(例: 500件)を付けると事故を防げます。また、並び替えキー createdAt はスキーマ側の timestamps: true 前提です(前コメントの確認参照)。

  • ルート実装(app/api/routes/dm.ts)が昇順前提か、クライアントがページングするかを確認してください。
  • DMMessagecreatedAt が存在すること(timestamps: true)を確認してください。

最小修正(ハード上限のみ)の例:

-  async listDMsBetween(user1: string, user2: string) {
-    const query = this.withTenant(
-      DMMessage.find({
-        $or: [
-          { from: user1, to: user2 },
-          { from: user2, to: user1 },
-        ],
-      }),
-    );
-    return await query.sort({ createdAt: 1 }).lean();
+  async listDMsBetween(user1: string, user2: string) {
+    const query = this.withTenant(
+      DMMessage.find({
+        $or: [
+          { from: user1, to: user2 },
+          { from: user2, to: user1 },
+        ],
+      }),
+    );
+    return await query.sort({ createdAt: 1 }).limit(500).lean();
   }

推奨修正(pairKey に移行し、さらに効率化):

-  async listDMsBetween(user1: string, user2: string) {
-    const query = this.withTenant(
-      DMMessage.find({
-        $or: [
-          { from: user1, to: user2 },
-          { from: user2, to: user1 },
-        ],
-      }),
-    );
-    return await query.sort({ createdAt: 1 }).lean();
+  async listDMsBetween(user1: string, user2: string) {
+    const pairKey = user1 < user2 ? `${user1}|${user2}` : `${user2}|${user1}`;
+    const query = this.withTenant(DMMessage.find({ pairKey }));
+    return await query.sort({ createdAt: 1 }).lean();
   }

さらに型活用の観点では、Promise<DMMessageDoc[]> のように戻り値型を固定しておくと呼び出し側の安全性が上がります。

必要ならページング対応(opts: { before/after/limit })へインターフェースとルートを含めた一括パッチを提案します。

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a5e7892 and 596d2b3.

⛔ Files ignored due to path filters (2)
  • app/client/deno.lock is excluded by !**/*.lock
  • app/takos_host/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • README.md (1 hunks)
  • app/api/DB/mod.ts (1 hunks)
  • app/api/DB/mongo.ts (2 hunks)
  • app/api/activity_handlers.ts (2 hunks)
  • app/api/models/takos/dm_message.ts (1 hunks)
  • app/api/models/takos/encrypted_message.ts (0 hunks)
  • app/api/models/takos/handshake_message.ts (0 hunks)
  • app/api/models/takos/key_package.ts (0 hunks)
  • app/api/models/takos_host/encrypted_message.ts (0 hunks)
  • app/api/models/takos_host/handshake_message.ts (0 hunks)
  • app/api/models/takos_host/key_package.ts (0 hunks)
  • app/api/routes/activitypub.ts (0 hunks)
  • app/api/routes/dm.ts (1 hunks)
  • app/api/routes/e2ee.ts (0 hunks)
  • app/api/routes/e2ee_test.ts (0 hunks)
  • app/api/routes/ws.ts (1 hunks)
  • app/api/server.ts (3 hunks)
  • app/api/utils/activitypub.ts (0 hunks)
  • app/client/deno.json (1 hunks)
  • app/client/src/components/Application.tsx (1 hunks)
  • app/client/src/components/Profile.tsx (1 hunks)
  • app/client/src/components/Setting/index.tsx (1 hunks)
  • app/client/src/components/chat/ChatRoomList.tsx (1 hunks)
  • app/client/src/components/chat/ChatSettingsOverlay.tsx (1 hunks)
  • app/client/src/components/chat/ChatTitleBar.tsx (0 hunks)
  • app/client/src/components/chat/FriendList.tsx (1 hunks)
  • app/client/src/components/chat/FriendRoomList.tsx (0 hunks)
  • app/client/src/components/e2ee/api.ts (0 hunks)
  • app/client/src/components/e2ee/binding.ts (0 hunks)
  • app/client/src/components/e2ee/mls_message.ts (0 hunks)
  • app/client/src/components/e2ee/mls_test.ts (0 hunks)
  • app/client/src/components/e2ee/mls_wrapper.ts (0 hunks)
  • app/client/src/components/e2ee/storage.ts (0 hunks)
  • app/client/src/components/e2ee/useMLS.ts (0 hunks)
  • app/client/src/components/microblog/api.ts (4 hunks)
  • app/client/src/utils/config.ts (0 hunks)
  • app/shared/db.ts (1 hunks)
  • app/takos_host/client/src/api.ts (0 hunks)
  • app/takos_host/client/src/pages/WelcomePage.tsx (3 hunks)
  • docs/chat_ux.md (1 hunks)
  • docs/key-sharing.md (1 hunks)
  • docs/multi-device-sync.md (1 hunks)
💤 Files with no reviewable changes (21)
  • app/client/src/utils/config.ts
  • app/api/models/takos_host/handshake_message.ts
  • app/api/models/takos/key_package.ts
  • app/client/src/components/e2ee/binding.ts
  • app/api/models/takos_host/key_package.ts
  • app/api/routes/activitypub.ts
  • app/client/src/components/e2ee/mls_test.ts
  • app/client/src/components/e2ee/useMLS.ts
  • app/api/routes/e2ee_test.ts
  • app/client/src/components/chat/ChatTitleBar.tsx
  • app/api/models/takos/handshake_message.ts
  • app/client/src/components/chat/FriendRoomList.tsx
  • app/api/utils/activitypub.ts
  • app/takos_host/client/src/api.ts
  • app/api/models/takos_host/encrypted_message.ts
  • app/api/models/takos/encrypted_message.ts
  • app/client/src/components/e2ee/mls_message.ts
  • app/api/routes/e2ee.ts
  • app/client/src/components/e2ee/storage.ts
  • app/client/src/components/e2ee/api.ts
  • app/client/src/components/e2ee/mls_wrapper.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: TypeScriptの型定義を活用する
エラーハンドリングを適切に実装する

Files:

  • app/client/src/components/chat/FriendList.tsx
  • app/client/src/components/Setting/index.tsx
  • app/client/src/components/Profile.tsx
  • app/api/models/takos/dm_message.ts
  • app/client/src/components/chat/ChatSettingsOverlay.tsx
  • app/api/routes/dm.ts
  • app/api/DB/mod.ts
  • app/api/DB/mongo.ts
  • app/api/routes/ws.ts
  • app/shared/db.ts
  • app/takos_host/client/src/pages/WelcomePage.tsx
  • app/api/server.ts
  • app/client/src/components/chat/ChatRoomList.tsx
  • app/client/src/components/microblog/api.ts
  • app/client/src/components/Application.tsx
  • app/api/activity_handlers.ts
app/api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

環境変数は常に getEnv(c) を用いてコンテキストから取得する

Files:

  • app/api/models/takos/dm_message.ts
  • app/api/routes/dm.ts
  • app/api/DB/mod.ts
  • app/api/DB/mongo.ts
  • app/api/routes/ws.ts
  • app/api/server.ts
  • app/api/activity_handlers.ts
app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Honoフレームワークのミドルウェアパターンに従う

Files:

  • app/api/models/takos/dm_message.ts
  • app/api/routes/dm.ts
  • app/api/DB/mod.ts
  • app/api/DB/mongo.ts
  • app/api/routes/ws.ts
  • app/api/server.ts
  • app/api/activity_handlers.ts
app/api/models/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

MongooseのスキーマでDBモデルを定義する

Files:

  • app/api/models/takos/dm_message.ts
app/api/{activity_handlers.ts,utils/activitypub*.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

ActivityPubの仕様に準拠したJSON-LD形式を使用する

Files:

  • app/api/activity_handlers.ts
🧠 Learnings (3)
📚 Learning: 2025-08-13T10:14:15.274Z
Learnt from: CR
PR: takoserver/takos#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T10:14:15.274Z
Learning: Applies to app/api/models/**/*.ts : MongooseのスキーマでDBモデルを定義する

Applied to files:

  • app/api/models/takos/dm_message.ts
  • app/api/DB/mongo.ts
📚 Learning: 2025-08-13T10:14:15.274Z
Learnt from: CR
PR: takoserver/takos#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T10:14:15.274Z
Learning: Applies to app/api/**/*.{ts,tsx} : Honoフレームワークのミドルウェアパターンに従う

Applied to files:

  • app/api/routes/dm.ts
📚 Learning: 2025-08-13T10:14:15.274Z
Learnt from: CR
PR: takoserver/takos#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T10:14:15.274Z
Learning: Applies to app/api/{activity_handlers.ts,utils/activitypub*.ts} : ActivityPubの仕様に準拠したJSON-LD形式を使用する

Applied to files:

  • app/api/DB/mongo.ts
  • app/client/src/components/microblog/api.ts
  • app/api/activity_handlers.ts
🧬 Code graph analysis (7)
app/api/models/takos/dm_message.ts (1)
app/api/models/plugins/tenant_scope.ts (1)
  • tenantScope (25-75)
app/client/src/components/chat/ChatSettingsOverlay.tsx (1)
app/client/src/components/chat/types.ts (1)
  • Room (21-39)
app/api/routes/dm.ts (3)
app/api/DB/mod.ts (1)
  • createDB (11-13)
app/shared/config.ts (1)
  • getEnv (49-56)
app/api/routes/ws.ts (1)
  • sendToUser (23-30)
app/takos_host/client/src/pages/WelcomePage.tsx (1)
app/takos_host/client/src/state.ts (2)
  • userNameState (6-6)
  • rootDomainState (11-11)
app/client/src/components/chat/ChatRoomList.tsx (1)
app/client/src/components/chat/FriendList.tsx (1)
  • FriendList (25-279)
app/client/src/components/microblog/api.ts (1)
app/api/services/user-info.ts (1)
  • UserInfo (6-12)
app/api/activity_handlers.ts (5)
app/api/DB/mod.ts (1)
  • createDB (11-13)
app/api/routes/ws.ts (2)
  • sendToUser (23-30)
  • broadcast (32-39)
app/api/utils/activitypub.ts (2)
  • deliverActivityPubObject (194-261)
  • getDomain (684-687)
app/api/DB/mongo.ts (1)
  • saveObject (51-117)
app/api/services/user-info.ts (2)
  • getUserInfo (79-184)
  • formatUserInfoForPost (315-345)
🔇 Additional comments (15)
app/client/src/components/chat/FriendList.tsx (1)

238-238: ワンライナー化は問題ありません(挙動不変)

クリックハンドラを簡潔にした変更は挙動を変えず可読性も向上しています。

app/client/src/components/Profile.tsx (1)

242-249: openChat の簡素化は新しい DM モデルと整合的です

部屋生成 API 呼び出しを排し、ハンドルへ正規化してチャット画面へ遷移するだけにしたのは、1:1 スレッドへ収束した今回の方針と一致しています。

app/takos_host/client/src/pages/WelcomePage.tsx (2)

1-3: onMount の導入と状態利用の差し替えは妥当です

onMount の追加と rootDomainState/userNameState の導入は今回の初期 DM 同期フローと整合的です。


197-201: Features の「プライベートDM」置換は方向性に合致

E2EE 強調からシンプル DM へコピーを刷新した点は、今回の PR 目的と一致しています。

app/api/models/takos/dm_message.ts (2)

11-11: テナントスコープの適用は適切

tenantScope プラグインで tenant_id を自動付与・クエリ強制している点は要件に合致しています。


13-17: モデル再利用パターンは妥当(Hot reload 耐性)

mongoose.models.DMMessage ?? mongoose.model(...) のパターンで重複登録を回避できており問題ありません。

app/client/src/components/Application.tsx (1)

35-40: registerUser の送信タイミングは安全に実装されています

registerUser は以下の仕組みで、WebSocket 接続前後いずれの場合も必ずユーザー登録メッセージを送信することが確認できました。

  • registerUser(user) 実行時に socket が未生成であれば、currentUser を更新したうえで connectWebSocket() を呼び出し、即座に返却
  • connectWebSocket() 内の socket.onopen ハンドラで、接続完了時に必ず currentUser(先に設定されたユーザー名)を送信
  • socket が存在し接続中(readyState !== OPEN)の場合は onopen ハンドラ、それ以外にすでに OPEN なら即時送信

以上により、接続確立前に呼び出された登録リクエストがロストすることはありません。追加の修正は不要と判断します。

app/client/src/components/microblog/api.ts (1)

409-417: クライアント側キャッシュマッピングのリファクタリングを推奨

  • setCachedUserInfo は同期関数のため、Promise.all で並列実行しても意味がありません。
  • 現状のように uncached[index] → fetchedInfos[index] の順序依存で割り当てると、サーバーの応答順がずれたり欠損した場合に別のユーザー情報を誤ってキャッシュするリスクがあります。

以下のように、レスポンス内の各 UserInfo から一意な識別子を合成してマッピングする実装をおすすめします。これにより順序に依存せず安全にキャッシュを更新できます。

-        await Promise.all(
-          fetchedInfos.map((info, index) =>
-            setCachedUserInfo(uncached[index], info)
-          ),
-        );
-
-        fetchedInfos.forEach((info, index) => {
-          fetchedMap[uncached[index]] = info;
-        });
+        for (const info of fetchedInfos) {
+          // isLocal フラグなどを使って一意のキーを合成
+          const id = info.isLocal
+            ? info.userName
+            : `${info.userName}@${info.domain}`;
+          setCachedUserInfo(id, info);
+          fetchedMap[id] = info;
+        }

あわせて、バックエンドの /api/users/batch エンドポイントが「要求順を固定した配列」を返しているのか、「キー付き(オブジェクト)で返しているのか」を確認してください。キー付きレスポンスに変更できる場合は、クライアント/サーバーともに識別子付きの仕様に揃えるとさらに堅牢になります。

  • バックエンド実装や API ドキュメントを参照し、返却データに ID フィールドが含まれているか確認してください。
  • 必要に応じてサーバー側もレスポンス仕様を見直し、クライアントと同じ識別子設計に合わせましょう。
app/api/DB/mod.ts (1)

8-8: 公開Surfaceの縮小は妥当です

startKeyPackageCleanupJob を公開から外し、必要最小限の export に整理されている点、OKです。

app/api/server.ts (3)

27-27: dm ルートの追加は妥当です

API 面の置換(e2ee → dm)に沿った適切なエクスポート/マウントです。


83-83: 公開側ルートの整理もOK

root側にdmを出していない点は意図通り(DMは /api 配下)。副作用なし。


55-76: /api/dm はミドルウェア配下でレート制限が適用されています

  • app/api/routes/dm.ts 内の環境変数取得は、30行目と50行目の2箇所で getEnv(c) が使用されていることを確認しました。
  • 直接 c.get('env') の呼び出しは存在しませんでした。

以上より、レート制限構成および環境変数取得はガイドラインに準拠しているため問題ありません。

app/api/activity_handlers.ts (2)

81-83: object の型チェックと早期 return は適切

Create/Noteの前提条件として妥当です。


158-188: 非DMフローの処理は概ねOK(保存→整形→タイムライン通知)

getUserInfoformatUserInfoForPostbroadcast/sendToUser の流れは既存設計と整合しています。

app/api/DB/mongo.ts (1)

21-21: DBインターフェースとMongoDB実装の整合性について
app/shared/db.tsDB インターフェースには saveDMMessage / listDMsBetween が定義されており、app/api/DB/mongo.tsMongoDB クラスで両メソッドが実装されています。他に implements DB しているクラスは存在しないため、現状型エラーや実行時未定義のリスクはありません。

戻り値に Promise<DMMessageDoc> のような型エイリアスを導入すると、呼び出し側の補完および型安全性がさらに向上します。

Comment on lines +141 to +155
const targetHost = (() => {
try {
return new URL(target).hostname;
} catch {
return "";
}
})();
if (targetHost && targetHost !== domain) {
deliverActivityPubObject([target], activity, actor, domain, env).catch(
(err) => {
console.error("Delivery failed:", err);
},
);
}
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

リモート配送の判定が acct:/user@domain 形式で機能しない(配送漏れ)

new URL(target) でホスト抽出しているため、acct:alice@example.comalice@example.com を受け取るケースで targetHost が空になり配送がスキップされます。ローカル/リモート判定を包括的にしてください。

修正差分:

-      const targetHost = (() => {
-        try {
-          return new URL(target).hostname;
-        } catch {
-          return "";
-        }
-      })();
-      if (targetHost && targetHost !== domain) {
+      const targetDomain = (() => {
+        try {
+          if (target.startsWith("http")) return new URL(target).hostname;
+          const acct = target.startsWith("acct:") ? target.slice(5) : target;
+          if (acct.includes("@")) return acct.split("@").pop() || "";
+        } catch (_) {}
+        return "";
+      })();
+      // ローカル以外は配送対象(acct:/user@domain も対象)
+      if (targetDomain && targetDomain !== domain) {
         deliverActivityPubObject([target], activity, actor, domain, env).catch(
           (err) => {
             console.error("Delivery failed:", err);
           },
         );
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const targetHost = (() => {
try {
return new URL(target).hostname;
} catch {
return "";
}
})();
if (targetHost && targetHost !== domain) {
deliverActivityPubObject([target], activity, actor, domain, env).catch(
(err) => {
console.error("Delivery failed:", err);
},
);
}
return;
const targetDomain = (() => {
try {
if (target.startsWith("http")) return new URL(target).hostname;
const acct = target.startsWith("acct:") ? target.slice(5) : target;
if (acct.includes("@")) return acct.split("@").pop() || "";
} catch (_) {}
return "";
})();
// ローカル以外は配送対象(acct:/user@domain も対象)
if (targetDomain && targetDomain !== domain) {
deliverActivityPubObject([target], activity, actor, domain, env).catch(
(err) => {
console.error("Delivery failed:", err);
},
);
}
return;
🤖 Prompt for AI Agents
In app/api/activity_handlers.ts around lines 141 to 155, the check that
determines remote delivery uses new URL(target) which returns empty for
acct:alice@example.com and plain alice@example.com so delivery is skipped;
update the host-extraction to handle three cases: 1) if target starts with
"acct:" parse the part after "acct:" and extract the domain after the last "@";
2) if target matches an email-like pattern (user@host) extract the host after
"@"; 3) otherwise attempt new URL(target). Normalize the extracted host (trim,
toLowerCase) and then compare to domain; if host exists and differs from domain,
call deliverActivityPubObject as before. Ensure the implementation handles
malformed inputs safely and falls back to "" on failure.

Comment thread app/api/DB/mongo.ts
import HandshakeMessage from "../models/takos/handshake_message.ts";
import HostHandshakeMessage from "../models/takos_host/handshake_message.ts";
import PendingInvite from "../models/takos/pending_invite.ts";
import DMMessage from "../models/takos/dm_message.ts";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DMMessage の導入は妥当。timestamps/インデックス/テナント対応の前提確認をお願いします

DMMessage の利用でDM保存がシンプルになっていて良いです。一方で本ファイルでは createdAt を前提に並び替えています(後述)。dm_message.ts 側で timestamps: truecreatedAt/updatedAt)と、ホストモードでのテナント分離用プラグイン($locals.env 依存)が有効になっているかを確認してください。合わせて検索性能のために (tenant_id, from, to, createdAt) もしくは後述の pairKey を使った複合インデックスの有無も要チェックです。

以下でスキーマ前提を確認できます:

必要なら dm_message.ts に最適なインデックス定義のパッチも用意します。


🏁 Script executed:

#!/bin/bash
# dm_message.ts のスキーマ定義・timestamps・index・テナント対応を確認
fd -a 'dm_message.ts' app | xargs -I{} sh -c 'echo "--- {} ---"; nl -ba "{}" | sed -n "1,220p"'
rg -nC2 -e 'timestamps\s*:\s*true' app/**/dm_message.ts
rg -nC2 -e '\.index\(' app/**/dm_message.ts
rg -nC3 -e '\$locals' app/**/dm_message.ts

Length of output: 518


先程のスクリプトでは nl コマンドが存在せず、また rg のファイルパス指定も誤っていたため出力が得られませんでした。以下のスクリプトで再度 dm_message.ts のスキーマ定義、timestamps、インデックス、テナント対応を確認してください。


🏁 Script executed:

#!/bin/bash
# dm_message.ts のスキーマ定義・timestamps・index・テナント対応を再確認

FILE=app/api/models/takos/dm_message.ts
if [ ! -f "$FILE" ]; then
  echo "Error: $FILE が存在しません"
  exit 1
fi

echo "--- $FILE ---"
# ファイル先頭200行を表示
cat -n "$FILE" | sed -n '1,200p'

echo "\n-- timestamps: true の設定を検索 --"
rg -nC2 -e 'timestamps\s*:\s*true' "$FILE"

echo "\n-- インデックス定義 (.index) を検索 --"
rg -nC2 -e '\.index\(' "$FILE"

echo "\n-- テナント分離 ($locals) を検索 --"
rg -nC3 -e '\$locals' "$FILE"

Length of output: 1330


timestamps オプション未設定かつ複合インデックス未定義のため、dm_message.ts 側は以下のポイントで要対応です。
次に、テナント分離プラグインの実装を確認し、$locals.env 依存やスキーマへのテナントフィールド追加をチェックしてください。


🏁 Script executed:

#!/bin/bash
# tenant_scope プラグイン実装を確認

FILE=app/api/models/plugins/tenant_scope.ts
if [ ! -f "$FILE" ]; then
  echo "Error: プラグインファイルが見つかりません: $FILE"
  exit 1
fi

echo "--- $FILE ---"
# ファイル先頭200行を表示
cat -n "$FILE" | sed -n '1,200p'

echo "\n-- \$locals(ドキュメント・クエリローカル)使用箇所を検索 --"
rg -nC2 -e '\$locals' "$FILE"

echo "\n-- envKey パラメータ使用箇所を検索 --"
rg -nC2 -e 'envKey' "$FILE"

echo "\n-- schema.add や index 定義を追加しているか検索 --"
rg -nC2 -e 'schema\.add' "$FILE"
rg -nC2 -e 'index\(' "$FILE"

Length of output: 3762


DMMessage スキーマに timestamps と複合インデックスを追加してください

DMMessage の導入自体は正しく、テナント分離プラグインも tenant_id を追加していますが、現状では以下の課題があります。

  • timestamps 未設定
    現在のスキーマでは createdAt を手動定義しているのみで、Mongoose の自動管理される timestamps オプション(createdAt/updatedAt)が使われていません。
  • 複合インデックス未定義
    テナント+送信者/受信者/作成日時での検索性能向上のため、(tenant_id, from, to, createdAt) の複合インデックスが必要です。

対応箇所は以下のとおりです。

  • app/api/models/takos/dm_message.ts

    • Schema 定義に { timestamps: true } を追加し、手動定義の createdAt を削除
    • 複合インデックスを追加
     import mongoose from "mongoose";
     import tenantScope from "../plugins/tenant_scope.ts";
     
  • const dmMessageSchema = new mongoose.Schema({

  • from: { type: String, required: true, index: true },

  • to: { type: String, required: true, index: true },

  • content: { type: String, required: true },

  • createdAt: { type: Date, default: Date.now },

  • });

  • const dmMessageSchema = new mongoose.Schema({

  • from: { type: String, required: true, index: true },

  • to: { type: String, required: true, index: true },

  • content: { type: String, required: true },

  • }, { timestamps: true });

  • // 複合インデックス(テナント・送信元・送信先・作成日時)

  • dmMessageSchema.index({

  • tenant_id: 1,

  • from: 1,

  • to: 1,

  • createdAt: 1,

  • });

    dmMessageSchema.plugin(tenantScope, { envKey: "ACTIVITYPUB_DOMAIN" });

    const DMMessage = mongoose.models.DMMessage ??
    mongoose.model("DMMessage", dmMessageSchema, "dm_messages");
    export default DMMessage;
    export { dmMessageSchema };


- app/api/models/plugins/tenant_scope.ts  
- プラグインは `tenant_id` フィールドと単一インデックスを正しく追加しています。  
- `options.envKey`(デフォルトでは `"ACTIVITYPUB_DOMAIN"`)が稼働環境で正しく設定されているかご確認ください。

以上を反映いただくことで、作成日時によるソート・検索性能の担保および自動更新されるタイムスタンプが有効化されます。  
必要に応じてパッチの作成をお手伝いしますのでご相談ください。  


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In app/api/models/takos/dm_message.ts (update schema definition) and review
app/api/models/plugins/tenant_scope.ts (envKey check): remove the manual
createdAt field from the DMMessage schema, enable Mongoose automatic timestamps
by passing { timestamps: true } to the Schema constructor, and add a composite
index on (tenant_id, from, to, createdAt) to improve tenant-scoped queries;
leave tenant_scope plugin behavior as-is (it already adds tenant_id and a single
index) but verify options.envKey is correctly set in your environment.


</details>

<!-- fingerprinting:phantom:poseidon:chinchilla -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +4 to +9
const dmMessageSchema = new mongoose.Schema({
from: { type: String, required: true, index: true },
to: { type: String, required: true, index: true },
content: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

スキーマのバリデーション強化とクエリ最適化(インデックス追加・timestamps化)

DM の主ユースケース(2ユーザー間の時系列取得)に対し、現状は単項インデックスのみでクエリ効率とスキーマ堅牢性が不足します。以下を推奨します。

  • content に trimmaxlength(例: 8KB 程度)を付与
  • from/to に trim と長さ制約(例: <= 2048)
  • timestampscreatedAt を自動付与(updatedAt は不要なら無効化)
  • 代表的クエリに効く複合インデックス(tenant_id, from, to, createdAt desc)

適用例:

-const dmMessageSchema = new mongoose.Schema({
-  from: { type: String, required: true, index: true },
-  to: { type: String, required: true, index: true },
-  content: { type: String, required: true },
-  createdAt: { type: Date, default: Date.now },
-});
+const dmMessageSchema = new mongoose.Schema(
+  {
+    from: { type: String, required: true, index: true, trim: true, minlength: 1, maxlength: 2048 },
+    to: { type: String, required: true, index: true, trim: true, minlength: 1, maxlength: 2048 },
+    content: { type: String, required: true, trim: true, maxlength: 8192 },
+  },
+  {
+    timestamps: { createdAt: true, updatedAt: false },
+    versionKey: false,
+  },
+);
+
+// 2者会話の時系列取得で効く複合インデックス
+dmMessageSchema.index({ tenant_id: 1, from: 1, to: 1, createdAt: -1 });
+// 逆方向の条件にもヒットしやすく(OR クエリが多い場合)
+dmMessageSchema.index({ tenant_id: 1, to: 1, from: 1, createdAt: -1 });

補足: 将来的にスパム対策で rate-limit/サイズ制限を API レイヤでも行うと万全です。

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/api/models/takos/dm_message.ts around lines 4–9, strengthen schema
validation and query performance: add trim and maxlength to content (e.g., 8192)
and trim/maxlength to from and to (e.g., 2048); add a tenant_id field (String,
required/indexed) if not present; enable mongoose timestamps (use timestamps: {
createdAt: true, updatedAt: false }) and remove manual createdAt field; and
create a compound index on { tenant_id: 1, from: 1, to: 1, createdAt: -1 } to
speed common two-user time-ordered queries. Ensure indexes are declared on the
schema and validation limits are applied as described.

Comment thread app/api/routes/dm.ts
// DM 用のシンプルな REST エンドポイント

const app = new Hono();
app.use("/dm/*", authRequired);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

/dm が未保護になるパス指定のバグ(認証バイパス)

app.use("/dm/*", authRequired) だと /dm 直下(POST/GET)はマッチせず未保護になります。高リスクです。/dm/dm/* の両方にミドルウェアを適用してください。

- app.use("/dm/*", authRequired);
+ app.use("/dm", authRequired);
+ app.use("/dm/*", authRequired);

🏁 Script executed:

#!/bin/bash
# Honoの他ルートでも同様の指定漏れが無いか確認
rg -nP "app\.use\(['\"]/[^'\"]+\*/['\"],\s*authRequired\)" app/api | sed -n '1,200p'
rg -nP "app\.(get|post|put|delete)\(['\"]/dm['\"]" app/api/routes/dm.ts -n -C2

Length of output: 175


dm.ts のルート定義を確認するため、ファイル先頭 200 行を出力します。


🏁 Script executed:

sed -n '1,200p' app/api/routes/dm.ts

Length of output: 1369


「/dm」エンドポイントが認証対象から漏れています

app.use("/dm/*", authRequired) だと、/dm(POST/GET)のパスにはマッチせず認証バイパスが発生します。高リスクな状態なので、以下のように /dm/dm/* の両方にミドルウェアを適用してください。

対象ファイル

  • app/api/routes/dm.ts

修正例

- app.use("/dm/*", authRequired);
+ // /dm 直下のエンドポイントにも認証を適用
+ app.use("/dm", authRequired);
+ // /dm/以下のサブパスにも認証を適用
+ app.use("/dm/*", authRequired);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use("/dm/*", authRequired);
// /dm 直下のエンドポイントにも認証を適用
app.use("/dm", authRequired);
// /dm/以下のサブパスにも認証を適用
app.use("/dm/*", authRequired);
🤖 Prompt for AI Agents
In app/api/routes/dm.ts around line 12, the middleware registration
app.use("/dm/*", authRequired) misses the exact "/dm" path so requests to "/dm"
bypass authentication; update the routing to apply authRequired to both "/dm"
and "/dm/*" (or equivalent pattern that covers both the base path and subpaths)
so that POST/GET to "/dm" are also protected.

Comment thread app/api/routes/dm.ts
Comment on lines +17 to +23
"json",
z.object({
from: z.string(),
to: z.string(),
content: z.string(),
}),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

送信者をリクエストBodyから受け取るのはなりすまし可能—認証主体から厳密に導出を

from をクライアント入力に依存すると、任意ユーザーを騙ったDM送信が可能です。認証ミドルウェアのコンテキストから送信者IDを取得し、Bodyからは外してください。加えてバリデーションを強化してください(長さ/空文字/上限など)。

例(概念 diff。authRequiredc.get("user"){ id: string } を格納する想定):

-  zValidator(
-    "json",
-    z.object({
-      from: z.string(),
-      to: z.string(),
-      content: z.string(),
-    }),
-  ),
+  zValidator(
+    "json",
+    z.object({
+      to: z.string().min(1).max(320),
+      content: z.string().min(1).max(2000),
+    }),
+  ),
   async (c) => {
-    const { from, to, content } = c.req.valid("json") as {
-      from: string;
-      to: string;
-      content: string;
-    };
+    const { to, content } = c.req.valid("json") as { to: string; content: string };
+    const auth = c.get("user") as { id: string } | undefined;
+    if (!auth?.id) return c.json({ error: "unauthorized" }, 401);
+    const from = auth.id;
     const db = createDB(getEnv(c));
-    const doc = await db.saveDMMessage(from, to, content) as { _id: string };
-    const payload = { id: doc._id, from, to, content };
+    try {
+      const doc = await db.saveDMMessage(from, to, content) as { _id: string };
+      const payload = { id: doc._id, from, to, content };
+      // オフラインでもsendToUserは黙ってreturnする想定でOK
+      sendToUser(to, { type: "dm", payload });
+      sendToUser(from, { type: "dm", payload });
+      return c.json(payload, 201);
+    } catch (e) {
+      return c.json({ error: "failed to persist DM" }, 500);
+    }
   },

Also applies to: 25-36


🏁 Script executed:

#!/bin/bash
# authRequired が user情報をどこへ格納するか確認(c.get('user') or c.var.user 等)
rg -n "authRequired" app/api | sed -n '1,200p'
rg -nP "c\.(get|var)\(['\"]user['\"]" app/api -n -C2

Length of output: 2266


🏁 Script executed:

#!/bin/bash
# Extract the auth middleware implementation to see where it stores authenticated user info
sed -n '1,200p' app/api/utils/auth.ts

Length of output: 901


🏁 Script executed:

#!/bin/bash
# Locate and show the implementation of createAuthMiddleware to see how it stores session/user info
rg -n 'createAuthMiddleware' -C50 shared/auth.ts

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Locate shared/auth.ts and print its contents to inspect how the middleware attaches the user to context
if [ -f shared/auth.ts ]; then
  sed -n '1,200p' shared/auth.ts
else
  echo "shared/auth.ts not found in root directory"
fi

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Locate implementation of createAuthMiddleware
rg -n "createAuthMiddleware" -C5 .
# Dump auth middleware implementation if found
if [ -f app/shared/auth.ts ]; then
  echo "--- app/shared/auth.ts ---"
  sed -n '1,200p' app/shared/auth.ts
fi
# Dump SessionDoc type definition
if [ -f app/shared/types.ts ]; then
  echo "--- app/shared/types.ts ---"
  sed -n '1,200p' app/shared/types.ts
fi

Length of output: 4897


DM送信エンドポイントの送信者をリクエストボディから取得しない

DM 送信時に from をクライアント入力に依存すると、他ユーザーになりすまして送信できる攻撃経路が残ります。必ず認証コンテキスト(セッション)からログインユーザーID を取得し、リクエストボディからは除外してください。また、tocontent のバリデーション(長さ・空文字防止など)も強化してください。

対象ファイル:

  • app/api/routes/dm.ts (行17–23付近)

変更案(概念 diff):

--- a/app/api/routes/dm.ts
@@
-  zValidator(
-    "json",
-    z.object({
-      from: z.string(),
-      to: z.string(),
-      content: z.string(),
-    }),
-  ),
+  zValidator(
+    "json",
+    z.object({
+      to: z.string().min(1).max(320),
+      content: z.string().min(1).max(2000),
+    }),
+  ),
@@
-  const { from, to, content } = c.req.valid("json") as {
-    from: string;
-    to: string;
-    content: string;
-  };
+  const { to, content } = c.req.valid("json") as {
+    to: string;
+    content: string;
+  };
+  // 認証ミドルウェアでセットされたユーザー情報を取得
+  const user = c.get("user") as { id: string } | undefined;
+  if (!user?.id) return c.json({ error: "unauthorized" }, 401);
+  const from = user.id;
   const db = createDB(getEnv(c));
-  const doc = await db.saveDMMessage(from, to, content) as { _id: string };
+  const doc = await db.saveDMMessage(from, to, content) as { _id: string };

加えて、現在の認証ミドルウェア実装ではセッション情報をコンテキストにセットしていないため、以下のように attach オプションを追加し、ユーザーID を c.set("user", { id: ... }) で登録できるようにしてください。

--- a/app/api/utils/auth.ts
@@ const authRequired: MiddlewareHandler = createAuthMiddleware<SessionDoc>({
   updateSession: async (session, expires, c) => {
     // セッション更新処理
   },
+  attach: (c, session) => {
+    // session に userId が含まれる前提
+    c.set("user", { id: (session as any).userId });
+  },
 });

以上の対応で、なりすましリスクを排除しつつバリデーションを強化できます。

🤖 Prompt for AI Agents
In app/api/routes/dm.ts around lines 17–23, the handler currently accepts a
`from` field from the request body which allows impersonation; remove `from`
from the request schema and instead read the authenticated user id from the
request context/session (e.g. c.get("user").id or similar) populated by the auth
middleware; tighten the schema to validate `to` (non-empty string, max length)
and `content` (non-empty, trimmed, max length) and enforce those constraints
before sending; also update the authentication middleware to use the attach
option so it sets the session user into the context (e.g. c.set("user",{id:
...})) so the route can reliably obtain the sender id from the server-side
session rather than client input.

Comment thread app/api/routes/dm.ts
Comment on lines +39 to +54
app.get(
"/dm",
zValidator(
"query",
z.object({ user1: z.string(), user2: z.string() }),
),
async (c) => {
const { user1, user2 } = c.req.valid("query") as {
user1: string;
user2: string;
};
const db = createDB(getEnv(c));
const messages = await db.listDMsBetween(user1, user2);
return c.json(messages);
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

DM取得APIにアクセス制御がありません—当事者以外が他人のDMを閲覧可能

GET /dm でも認証主体が user1/user2 のいずれかであることを確認してください。確認が無いと第三者が任意の2者間DMを取得できます。

例:

   async (c) => {
     const { user1, user2 } = c.req.valid("query") as {
       user1: string;
       user2: string;
     };
+    const auth = c.get("user") as { id: string } | undefined;
+    if (!auth?.id) return c.json({ error: "unauthorized" }, 401);
+    if (auth.id !== user1 && auth.id !== user2) {
+      return c.json({ error: "forbidden" }, 403);
+    }
     const db = createDB(getEnv(c));
-    const messages = await db.listDMsBetween(user1, user2);
-    return c.json(messages);
+    try {
+      const messages = await db.listDMsBetween(user1, user2);
+      return c.json(messages);
+    } catch (_e) {
+      return c.json({ error: "failed to load DMs" }, 500);
+    }
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.get(
"/dm",
zValidator(
"query",
z.object({ user1: z.string(), user2: z.string() }),
),
async (c) => {
const { user1, user2 } = c.req.valid("query") as {
user1: string;
user2: string;
};
const db = createDB(getEnv(c));
const messages = await db.listDMsBetween(user1, user2);
return c.json(messages);
},
);
app.get(
"/dm",
zValidator(
"query",
z.object({ user1: z.string(), user2: z.string() }),
),
async (c) => {
const { user1, user2 } = c.req.valid("query") as {
user1: string;
user2: string;
};
// Ensure the caller is authenticated
const auth = c.get("user") as { id: string } | undefined;
if (!auth?.id) {
return c.json({ error: "unauthorized" }, 401);
}
// Only allow if the caller is one of the two participants
if (auth.id !== user1 && auth.id !== user2) {
return c.json({ error: "forbidden" }, 403);
}
const db = createDB(getEnv(c));
try {
const messages = await db.listDMsBetween(user1, user2);
return c.json(messages);
} catch (_e) {
return c.json({ error: "failed to load DMs" }, 500);
}
},
);
🤖 Prompt for AI Agents
In app/api/routes/dm.ts around lines 39–54, the GET /dm handler lacks access
control allowing any caller to fetch any two users' DMs; retrieve the
authenticated user id from the request/context (e.g., from c.req.user,
c.getAuthUser(), or the framework auth helper used elsewhere), verify that the
authenticated id equals either user1 or user2 from the validated query, and if
not return a 403 Forbidden response before calling db.listDMsBetween; keep
validation and existing DB call intact and add the early auth check and error
response.

Comment thread app/client/deno.json
"zod": "npm:zod@^3.24.4",
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
"ts-mls": "npm:ts-mls@^1.1.0"
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

bcrypt の重複・混在(bcryptjs と deno.land/x/bcrypt)を解消してください

クライアント側の imports に bcryptjsbcrypt(deno.land/x)が同時に存在しています。バンドル環境(Vite)とランタイム(ブラウザ/Tauri)での互換性や重複サイズ増を招く懸念があります。現状どちらが実際に参照されているかを確認のうえ、片方に統一をお願いします。フロントエンドでの用途なら純 JS 実装の bcryptjs で十分なケースが多いです。

以下のスクリプトでクライアント配下での使用箇所を洗い出せます(使用されていなければ imports から削除可能です)。


🏁 Script executed:

#!/bin/bash
# クライアント配下での bcrypt 利用状況確認
rg -n --type=ts --type=tsx -C2 -g 'app/client/**' -P '(from\s+[\'"]bcryptjs[\'"]|from\s+[\'"]bcrypt[\'"]|import\([\'"]bcrypt(js)?[\'"]\))'

Length of output: 321


以下の簡易スクリプトで再度クライアント配下の bcryptbcryptjs 利用箇所を洗い出してみてください:


🏁 Script executed:

#!/bin/bash
# クライアント配下での bcrypt 利用状況確認 (シンプル版)
rg -n --type=ts --type=tsx -C2 -g 'app/client/**' \
  -e 'import.*bcrypt' \
  -e 'import\(.*bcrypt'

Length of output: 121


以下のスクリプトで、app/client 配下のあらゆる拡張子ファイルを対象に bcryptjs および deno.land/x/bcrypt の使用箇所を洗い出してみてください。


🏁 Script executed:

#!/bin/bash
# クライアント配下での bcryptjs / deno bcrypt 利用状況確認

echo "🔍 bcryptjs の検索"
rg -n -g 'app/client/**/*.{ts,tsx,js,jsx}' 'bcryptjs' || true

echo
echo "🔍 deno.land/x/bcrypt の検索"
rg -n -g 'app/client/**/*.{ts,tsx,js,jsx}' 'deno\.land\/x\/bcrypt' || true

Length of output: 407


以下のスクリプトで、deno.land/x/bcrypt の固定文字列検索を行い、クライアント配下での使用有無を再確認してください。


🏁 Script executed:

#!/bin/bash
# クライアント配下での deno.land/x/bcrypt 利用状況確認(固定文字列検索)
echo "🔍 deno.land/x/bcrypt の検索(固定文字列)"
rg -n -F 'deno.land/x/bcrypt' -g 'app/client/**/*.{ts,tsx,js,jsx}' || true

Length of output: 148


app/client/deno.json の不要な bcrypt エントリを削除してください

app/client/src/utils/crypto.ts では純粋に bcryptjsimport { hashSync } from "bcryptjs";)のみが利用されており、deno.land/x/bcrypt"https://deno.land/x/bcrypt@v0.4.1/mod.ts")は一切参照されていません。クライアント側に未使用の依存を残さないよう、以下の 1 行を削除してください。

• 削除対象

  • ファイル:app/client/deno.json
  • 行番号:およそ 49 行目
  • 現在の内容
    "bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts"

— これにより、重複依存の解消とバンドルサイズ最適化が図れます。

🤖 Prompt for AI Agents
In app/client/deno.json around line 49, remove the unused bcrypt dependency
entry (the line: "bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts") because
client code uses bcryptjs only; edit the JSON to delete that single key/value
pair and ensure the file remains valid JSON (no trailing commas), then run your
dependency/bundle check (e.g., deno cache or your build) to confirm no
references remain.

@@ -1,8 +1,8 @@
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
import { createEffect, createSignal, onMount } from "solid-js";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Show の未インポートによりビルドエラー(JSX 未解決コンポーネント)

このファイル内で <Show> を多数使用していますが、solid-js からの Show インポートが削除されています。ビルド/実行時に未定義となります。

適用差分案:

-import { createEffect, createSignal, onMount } from "solid-js";
+import { createEffect, createSignal, onMount, Show } from "solid-js";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createEffect, createSignal, onMount } from "solid-js";
// app/client/src/components/Application.tsx
import { createEffect, createSignal, onMount, Show } from "solid-js";
🤖 Prompt for AI Agents
In app/client/src/components/Application.tsx around lines 1 to 1, the component
uses <Show> in many places but the Show import from "solid-js" is missing; add
Show to the named imports from "solid-js" (e.g. import { createEffect,
createSignal, onMount, Show } from "solid-js") so the JSX component is defined
and the build error is resolved.

Comment on lines 197 to 201
icon: Lock,
title: "ac",
desc:
"activitypub-e2eeを利用したエンドツーエンド暗号化。プライバシーを最優先に考えた設計。",
highlight: "セキュア",
title: "プライベートDM",
desc: "シンプルなダイレクトメッセージで気軽にやり取りできます。",
highlight: "プライベート",
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

比較表の E2EE 表記が仕様と不一致(ユーザー向け誤情報)

本 PR で MLS/E2EE を撤去したにもかかわらず、下段の比較表(COMPARISON)で「E2EE: takos=○」のままです。実装と矛盾するため更新してください(例: takos: "×" あるいは将来計画なら注記)。

更新イメージ(参考・比較表側の定数定義を修正):

{
  label: "E2EE (エンドツーエンド暗号化)",
  takos: "×", // 現状はプレーンテキスト DM
  others: { Mastodon: "×", Misskey: "×", Twitter: "×", Facebook: "×" },
}
🤖 Prompt for AI Agents
In app/takos_host/client/src/pages/WelcomePage.tsx around lines 197 to 201, the
comparison table still labels takos as E2EE ("○") which contradicts the current
implementation that no longer provides MLS/E2EE; update the comparison table's
constant for the E2EE row to reflect the correct state (set takos to "×" or
replace with a string that notes it's planned/not implemented) and ensure other
vendors remain correct, keeping the label "E2EE (エンドツーエンド暗号化)" and adjusting any
UI text or tests that assert the old value.

Comment thread README.md
Comment on lines +172 to +176
## DM API

平文で1対1のDMを送受信するためのシンプルなAPIです。メッセージはサーバーに
保存され、当事者へWebSocketで通知されます。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DMは平文であることとプライバシー上の注意点を明記してください

E2EEを外した変更の要点なので、利用者が誤解しないよう「保存時・配送時とも暗号化されない」「サーバ管理者・侵入者・バックアップ媒体から閲覧され得る」等の注意喚起を追記してください。

提案差分:

 ## DM API
-平文で1対1のDMを送受信するためのシンプルなAPIです。メッセージはサーバーに
-保存され、当事者へWebSocketで通知されます。
+平文で1対1のDMを送受信するためのシンプルなAPIです。メッセージはサーバーに
+保存され、当事者へWebSocketで通知されます。
+
+注意:
+- 本機能はエンドツーエンド暗号化(E2EE)ではありません。サーバー保存時・配送時ともに平文です。
+- サーバー管理者や侵害時の第三者が内容を閲覧可能である点を理解した上でご利用ください。
+- 機微情報の送信は推奨しません。
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## DM API
平文で1対1のDMを送受信するためのシンプルなAPIです。メッセージはサーバーに
保存され、当事者へWebSocketで通知されます。
## DM API
平文で1対1のDMを送受信するためのシンプルなAPIです。メッセージはサーバーに
保存され、当事者へWebSocketで通知されます。
注意:
- 本機能はエンドツーエンド暗号化(E2EE)ではありません。サーバー保存時・配送時ともに平文です。
- サーバー管理者や侵害時の第三者が内容を閲覧可能である点を理解した上でご利用ください。
- 機微情報の送信は推奨しません。
🤖 Prompt for AI Agents
In README.md around lines 172 to 176, the DM API section currently states
messages are plain-text but lacks explicit privacy warnings; update this section
to clearly state that messages are not encrypted at rest or in transit, that
server operators, attackers who gain access, and backup media can read stored
messages, and advise users that sensitive information should not be sent via
this DM system; add a short, prominent caution paragraph or bullet points
immediately after the existing description with those exact points and a
recommendation to use E2EE for sensitive content.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant