トップブログ

商談ルームの入り口に、穴が3つ空いていた

2026年9月6日

入室まわりを点検して見つかった3つの抜けと、それぞれ変更した内容
入室まわりを点検して見つかった3つの抜けと、それぞれ変更した内容

オンライン商談のURLは、渡された人以外にも回ります。転送されたメールから開かれることもあれば、前回と同じURLを使い回していれば、関係のない人が入ってこられます。だから入室には条件を付けます。

2026年8月19日に Meetnect のコードを4領域に分けて自分たちで読み直したとき、その条件が思っていた通りには効いていない箇所が3つ出てきました。この記事は、その3つが何で、いま何行を足して塞いだかの記録です。どれも設計の話ではなく、書き忘れの話です。


この記事の内容
  1. 入室の条件は、もともと3つある
  2. 穴①:待っている間に、発言できていた
  3. 穴②:お客様が先に来ると、承認制が消えていた
  4. 穴③:他社と同じルームIDが取れていた
  5. 承認できるのは、社内で商談に出ている人だけ
  6. 入り口が2つあると、片方だけ条件が抜ける

入室の条件は、もともと3つある

Meetnect の商談ルームは、入り口で3つを見ています。

3つ目の承認だけ、他の2つと違うところがあります。待たせている間も、通信そのものはつながっているという点です。ここが穴になりました。

穴①:待っている間に、発言できていた

承認待ちのお客様は、画面の上では「主催者の承認を待っています」と出したまま止めています。ただ、止めていたのはその画面だけでした。

商談ルームは WebSocket という常時つないだ通信の上で動いています。承認を待っている状態でも接続は生きているので、その接続にチャットやスタンプ、ホワイトボードの操作、さらに映像をつなぐための通信を流せていました。 画面を出さないことと、送れなくすることは別の作業です。

いまは、受け取った側で1行だけ先に見ます。

if (self.pending) return; // 承認待ちのお客様は、承認されるまで一切のメッセージを送れない

配る側にも同じ条件を入れました。全員に配る処理で、承認待ちの人は飛ばします。

for (const p of this.peers.values()) { if (p.id === exceptId || p.pending) continue; ... }

この2つで、承認待ちの間は送れないし、届かない状態になります。参加者の一覧にも入らないので、先に入っている人の画面にはまだ現れません。映像をつなぐ交渉が始まるのは承認の後なので、待っている人の側に相手の映像や音声が来ることもありません。

待合室は、画面の上ではなく通信の側で作らないと意味がない、という話でした。

穴②:お客様が先に来ると、承認制が消えていた

2つ目は順番の問題です。

承認制にするかどうかはルームごとの設定で、商談ルームの側がデータベースから読み込みます。この読み込みが接続の最初に1回だけで、あとはその値を使い回していました。

問題は、ルームの登録より先にお客様が繋いだときです。招待URLを受け取ったお客様が主催者より早く開くと、その時点ではルームの行がまだ無いので、承認制もドメイン制限も「無し」として読まれます。そのあと主催者がルームを登録しても、読み直す処理がありません。 先に来た人が決めた「無し」が、その商談のあいだ残ります。

いまは、入室を名乗る処理のたびに読み直しています。

// ルーム設定(承認制・ドメイン)を毎回最新化:
// ゲストがホスト登録より先に接続してもキャッシュが陳腐化しない
const row = await this.env.DB.prepare(
  "SELECT require_approval, domain FROM rooms WHERE id=? ORDER BY created_at DESC LIMIT 1"
).bind(rid).first();

1回で済ませたくなるのは、毎回データベースを引くのがもったいないからです。実際には入室のたびに1回引くだけで、商談中に何度も走る処理ではありませんでした。節約する場所を間違えていたという方が近いです。

穴③:他社と同じルームIDが取れていた

3つ目は、部屋の名前そのものです。

ルームIDは覚えやすい文字列を自分で決められるようにしています。このIDが全社で1つの名前空間でした。A社が sales を使っていて、B社も sales を作れる。同じIDのURLを開くと、同じ部屋につながります。

いまは、IDを指定して作るときに他社が使っていないかを見て、使われていれば409で断ります。自社が同じIDを再利用するのは通します(毎週同じ部屋を使う運用のため)。

const dup = await env.DB.prepare("SELECT domain FROM rooms WHERE id=? LIMIT 1").bind(custom).first();
if (dup && dup.domain !== s.domain) {
  return json({ error: "id_taken", message: "このルームIDは既に使われています。別のIDにしてください。" }, 409);
}

あわせて、指定できる形も絞りました。半角英小文字・数字・ハイフンで3〜40文字、先頭は英数字、そして r- で始まる形は使えません。r- は自動生成のIDに使っている頭文字で、自動生成側と衝突させないための予約です。

承認できるのは、社内で商談に出ている人だけ

承認の権限も、通信の側で見ています。承認と拒否を受け付けるのは、次の条件を全部満たす人だけです。

if (!(self.internal && self.role === "active")) return;

社内(主催または見学)で、かつ商談に出ている側。見学モードで入っている人は承認できません。 見学は映像も音声も送らず、お客様の画面には現れない立場なので、外から人を入れる判断はしない側に置きました。

拒否したときは、待っているお客様に「入室が承認されませんでした」と表示して接続を閉じ、待機の一覧から消します。承認する側が後から入ってきた場合は、その時点で待っている人の一覧をまとめて受け取ります。先に待たせておいて、あとから主催者が入るという順番でも取りこぼしません。

入り口が2つあると、片方だけ条件が抜ける

最後に、まだ直していないものを1つ書いておきます。

同時に話せるのは4名までで、5人目の接続は room-full で断っています。この人数の確認をしているのは、承認を経ずに入ってくる経路だけです。承認して正式に参加させる処理の側には、同じ確認が入っていません。

承認を経ずに入る経路には4名までの人数確認があり、承認して入れる経路には同じ確認が入っていない
承認を経ずに入る経路には4名までの人数確認があり、承認して入れる経路には同じ確認が入っていない

つまり、4名が入っている商談で待機中のお客様を承認すると、5人目として入れてしまいます。承認制をオンにしているかどうかで、通る道が変わるからです。塞いだ3つと同じ形で、入り口が2つあるのに条件を片方にしか書かなかったという抜け方をしています。ここは直す側の宿題です。

3つとも、悪い設計から出た穴ではありませんでした。条件は決まっていて、書く場所が足りていなかっただけです。決めたことが本当に効いているかは、決めた側を読んでも分かりません。 通る道を1本ずつ数えるしかない、というのが今回の結論です。


Meetnect はブラウザだけで動くオンライン商談ツールです。入室の承認は既定でオンになっていて、社外のお客様は承認されるまで商談の中身に触れません。この記事のコードと数字は、すべて実際に動いている実装から書き出したものです。

商談しながら、議事録ができています

Meetnect はブラウザだけで動くオンライン商談ツールです。相手は登録不要、URLを開くだけ。通話中に文字起こしが進み、終わった時点で「誰が何を言ったか」まで残った議事録ができています。7日間、カード登録なしで試せます。