[{"data":1,"prerenderedAt":12114},["ShallowReactive",2],{"seo-locale-availability:\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide":3,"post:ja:\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide":7,"surroundings:ja":798},[4,5,6],"zh-CN","en","ja",{"post":8,"contentLocale":6,"isFallback":775},{"id":9,"title":10,"accent":11,"body":12,"description":774,"draft":775,"experience":776,"extension":777,"featured":778,"image":779,"meta":785,"navigation":778,"path":786,"publishedAt":787,"readingTime":788,"seo":789,"stem":790,"tags":791,"updatedAt":796,"video":776,"__hash__":797},"posts_ja\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide.md","TunnelDeck デスクトップ版ガイド：ssh -L を保存できるトンネルへ","mint",{"type":13,"value":14,"toc":747},"minimark",[15,29,32,67,77,80,85,96,118,133,137,140,213,228,236,240,247,252,255,293,296,315,320,339,343,354,361,383,400,404,407,428,443,447,450,502,516,520,523,526,529,536,539,550,557,561,564,571,575,578,596,599,606,610,613,616,619,622,625,644,648,651,654,660,663,669,672,675,678,685,700,704,716,719,740,743],[16,17,18,19,23,24,28],"p",{},"SSH のローカルポート転送は便利ですが、コマンドが増えるとポート、転送先、秘密鍵の場所を管理しにくくなります。",[20,21,22],"strong",{},"TunnelDeck"," は ",[25,26,27],"code",{},"ssh -L"," を保存・編集し、すぐに開始／停止できる軽量なクロスプラットフォームデスクトップアプリです。ホスト指紋の確認、OS の認証情報ストア、ローカルのみの待ち受けといった重要な安全策も維持します。",[16,30,31],{},"たとえば、次のコマンドをそのままインポートできます。",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"language-bash shiki shiki-themes github-dark github-dark","ssh -L 9108:127.0.0.1:9108 -p 33899 root@ssh.example.com\n","bash","",[25,40,41],{"__ignoreMap":38},[42,43,46,50,54,58,61,64],"span",{"class":44,"line":45},"line",1,[42,47,49],{"class":48},"sFR8T","ssh",[42,51,53],{"class":52},"s8ozJ"," -L",[42,55,57],{"class":56},"s4wv1"," 9108:127.0.0.1:9108",[42,59,60],{"class":52}," -p",[42,62,63],{"class":52}," 33899",[42,65,66],{"class":56}," root@ssh.example.com\n",[16,68,69,70,73,74,76],{},"接続後、手元の PC から ",[25,71,72],{},"127.0.0.1:9108"," を開くと、通信は SSH サーバーを経由し、SSH サーバーから見た ",[25,75,72],{}," へ転送されます。",[16,78,79],{},"この記事ではデスクトップ版 v0.3.1 を中心に、インストール、接続の作成、パスワード／秘密鍵認証、初回の指紋確認、ブラウザーショートカット、トラブルシューティングまで順に説明します。",[81,82,84],"h2",{"id":83},"tunneldeck-が向いている用途","TunnelDeck が向いている用途",[16,86,87,88,95],{},"現在の TunnelDeck は SSH の ",[20,89,90,91,94],{},"ローカルポート転送（",[25,92,93],{},"-L","）"," に特化しています。",[97,98,99,103,106,109,115],"ul",{},[100,101,102],"li",{},"サーバーのループバックアドレスだけで待ち受ける管理画面へのアクセス；",[100,104,105],{},"踏み台サーバー経由でのデータベース、開発サービス、社内 Web アプリへのアクセス；",[100,107,108],{},"よく使う複数のトンネルを保存し、必要なものだけを開始；",[100,110,111,112,114],{},"毎回長い ",[25,113,27],{}," コマンドを組み立て直さずに接続；",[100,116,117],{},"Web 用トンネルを、明示的な操作後にだけ既定ブラウザーで開く。",[16,119,120,121,124,125,128,129,132],{},"VPN ではなく、リモート転送 ",[25,122,123],{},"-R","、SOCKS 動的転送 ",[25,126,127],{},"-D","、リモートシェルは提供しません。インポーターは、外部コマンドを実行し得る ",[25,130,131],{},"ProxyCommand"," なども拒否します。",[81,134,136],{"id":135},"_4-つの接続先を理解する","4 つの接続先を理解する",[16,138,139],{},"冒頭のコマンドは次の設定に対応します。",[141,142,143,159],"table",{},[144,145,146],"thead",{},[147,148,149,153,156],"tr",{},[150,151,152],"th",{},"項目",[150,154,155],{},"例",[150,157,158],{},"意味",[160,161,162,176,189,201],"tbody",{},[147,163,164,168,173],{},[165,166,167],"td",{},"SSH サーバー",[165,169,170],{},[25,171,172],{},"ssh.example.com:33899",[165,174,175],{},"TunnelDeck がログインする SSH ホストとポート",[147,177,178,181,186],{},[165,179,180],{},"SSH ユーザー",[165,182,183],{},[25,184,185],{},"root",[165,187,188],{},"SSH のログインアカウント",[147,190,191,194,198],{},[165,192,193],{},"ローカル入口",[165,195,196],{},[25,197,72],{},[165,199,200],{},"手元の PC 上でアプリが接続するアドレス",[147,202,203,206,210],{},[165,204,205],{},"リモート転送先",[165,207,208],{},[25,209,72],{},[165,211,212],{},"SSH サーバーから見た転送先",[16,214,215,216,219,220,223,224,227],{},"最後の項目が特に重要です。リモート転送先の ",[25,217,218],{},"127.0.0.1"," は、TunnelDeck を動かす PC ではなく ",[20,221,222],{},"SSH サーバー自身"," を指します。サービスが SSH サーバーから到達できる別の LAN ホストにある場合は、たとえば ",[25,225,226],{},"10.0.0.20:8080"," のように、そのホストのアドレスを設定します。",[33,229,234],{"className":230,"code":232,"language":233,"meta":38},[231],"language-text","ローカルアプリ\n  └─ 127.0.0.1:9108\n       └─ 暗号化 SSH 接続 → ssh.example.com:33899\n            └─ SSH サーバーから接続 → 127.0.0.1:9108\n","text",[25,235,232],{"__ignoreMap":38},[81,237,239],{"id":238},"_1-デスクトップ版をインストールする","1. デスクトップ版をインストールする",[16,241,242,243,246],{},"現在、プロジェクトには Apple／Windows のコード署名 ID がないため、v0.3.1 では ",[20,244,245],{},"未署名のビルド済みデスクトップインストーラーを配布していません","。インストールスクリプトが環境を確認し、固定リリースのソースを取得してローカルでビルドします。未署名バイナリのために Gatekeeper や SmartScreen を回避する必要はありません。",[248,249,251],"h3",{"id":250},"macos-と-linux","macOS と Linux",[16,253,254],{},"まず依存関係だけを確認します。",[33,256,258],{"className":35,"code":257,"language":37,"meta":38,"style":38},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh \\\n  | sh -s -- --check\n",[25,259,260,274],{"__ignoreMap":38},[42,261,262,265,268,271],{"class":44,"line":45},[42,263,264],{"class":48},"curl",[42,266,267],{"class":52}," -fsSL",[42,269,270],{"class":56}," https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh",[42,272,273],{"class":52}," \\\n",[42,275,277,281,284,287,290],{"class":44,"line":276},2,[42,278,280],{"class":279},"sOPea","  |",[42,282,283],{"class":48}," sh",[42,285,286],{"class":52}," -s",[42,288,289],{"class":52}," --",[42,291,292],{"class":52}," --check\n",[16,294,295],{},"出力を確認してからインストールします。",[33,297,299],{"className":35,"code":298,"language":37,"meta":38,"style":38},"curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[25,300,301],{"__ignoreMap":38},[42,302,303,305,307,309,312],{"class":44,"line":45},[42,304,264],{"class":48},[42,306,267],{"class":52},[42,308,270],{"class":56},[42,310,311],{"class":279}," |",[42,313,314],{"class":48}," sh\n",[16,316,317,319],{},[25,318,264],{}," がない場合：",[33,321,323],{"className":35,"code":322,"language":37,"meta":38,"style":38},"wget -qO- https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.sh | sh\n",[25,324,325],{"__ignoreMap":38},[42,326,327,330,333,335,337],{"class":44,"line":45},[42,328,329],{"class":48},"wget",[42,331,332],{"class":52}," -qO-",[42,334,270],{"class":56},[42,336,311],{"class":279},[42,338,314],{"class":48},[248,340,342],{"id":341},"windows-powershell","Windows PowerShell",[33,344,348],{"className":345,"code":346,"language":347,"meta":38,"style":38},"language-powershell shiki shiki-themes github-dark github-dark","irm https:\u002F\u002Fraw.githubusercontent.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fv0.3.1\u002Finstall.ps1 | iex\n","powershell",[25,349,350],{"__ignoreMap":38},[42,351,352],{"class":44,"line":45},[42,353,346],{},[16,355,356,357,360],{},"インストーラーは Go 1.25+、Node.js 20+、各 OS のビルド依存関係を確認します。Go または Node.js がない場合は公式 SHA-256 を検証し、TunnelDeck 専用のユーザーディレクトリへ配置します。グローバルの ",[25,358,359],{},"PATH"," は変更しません。",[97,362,363,370,376],{},[100,364,365,366,369],{},"macOS では Xcode Command Line Tools も必要で、",[25,367,368],{},"~\u002FApplications\u002FTunnelDeck.app"," にインストール；",[100,371,372,373,369],{},"Linux では GTK3 と WebKitGTK 4.1 が必要で、",[25,374,375],{},"~\u002F.local\u002Fbin\u002FTunnelDeck",[100,377,378,379,382],{},"Windows では WebView2 が必要で、",[25,380,381],{},"%LOCALAPPDATA%\\Programs\\TunnelDeck\\"," とスタートメニューに登録。",[16,384,385,386,393,394,399],{},"リモートスクリプトをシェルへ渡す前に、",[387,388,392],"a",{"href":389,"rel":390},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Finstall.sh",[391],"nofollow","v0.3.1 のインストールスクリプト","を確認してください。全依存関係と手動ビルド手順は",[387,395,398],{"href":396,"rel":397},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Fblob\u002Fv0.3.1\u002Fdocs\u002FSOURCE_INSTALL.md",[391],"ソースインストールガイド","にあります。",[81,401,403],{"id":402},"_2-既存の-ssh-l-コマンドをインポートする","2. 既存の ssh -L コマンドをインポートする",[16,405,406],{},"すでに動作しているコマンドがある場合：",[408,409,410,413,419,422,425],"ol",{},[100,411,412],{},"TunnelDeck を開き、「コマンドをインポート」を選択；",[100,414,415,416,418],{},"完全な ",[25,417,27],{}," コマンドを貼り付け；",[100,420,421],{},"解析された SSH ホスト、ポート、ユーザー、ローカル入口、転送先を確認；",[100,423,424],{},"分かりやすいプロファイル名を設定；",[100,426,427],{},"認証方法を選び、保存して開始。",[16,429,430,431,434,435,437,438,437,440,442],{},"TunnelDeck は引数を解析し、Go の SSH ライブラリから直接接続します。入力を Shell コマンドへ連結して",[20,432,433],{},"実行することはありません","。監査しやすい範囲に保つため、1 つのローカル転送だけを受け付け、リモートコマンド、",[25,436,123],{},"、",[25,439,127],{},[25,441,131],{}," は拒否します。",[81,444,446],{"id":445},"_3-トンネルを手動で作成する","3. トンネルを手動で作成する",[16,448,449],{},"「新規」を選び、次の順に入力します。",[408,451,452,458,472,478,487,493,499],{},[100,453,454,457],{},[20,455,456],{},"名前","：例「開発環境の管理画面」；",[100,459,460,463,464,467,468,471],{},[20,461,462],{},"SSH サーバーとポート","：例 ",[25,465,466],{},"ssh.example.com"," と ",[25,469,470],{},"33899","；",[100,473,474,477],{},[20,475,476],{},"SSH ユーザー名","：サーバーで許可されたアカウント；",[100,479,480,483,484,486],{},[20,481,482],{},"ローカル待ち受けアドレスとポート","：通常は ",[25,485,218],{}," のまま、空いているポートを指定；",[100,488,489,492],{},[20,490,491],{},"リモート転送先とポート","：SSH サーバーから到達できるサービス；",[100,494,495,498],{},[20,496,497],{},"認証方法","：パスワードまたは SSH 秘密鍵；",[100,500,501],{},"「保存して開始」を選択。",[16,503,504,505,507,508,511,512,515],{},"既定の ",[25,506,218],{}," では、同じ PC 上のプログラムだけがトンネルを利用できます。LAN 内の別端末にも公開する明確な理由があり、OS のファイアウォールも設定済みの場合を除き、",[25,509,510],{},"0.0.0.0"," や ",[25,513,514],{},"::"," へ変更しないでください。TunnelDeck も公開リスクを警告します。",[81,517,519],{"id":518},"_4-パスワード認証と秘密鍵認証","4. パスワード認証と秘密鍵認証",[248,521,522],{"id":522},"パスワード",[16,524,525],{},"SSH のログインパスワードを入力します。「認証情報を記憶」は既定のまま無効にするのが基本です。その場合、パスワードは現在の実行中だけメモリに保持され、トンネル停止時にトンネルオブジェクトから消去されます。",[248,527,528],{"id":528},"秘密鍵",[16,530,531,532,535],{},"秘密鍵ファイルを選択します。暗号化された鍵ならパスフレーズも入力します。公開鍵は、あらかじめサーバー側アカウントの ",[25,533,534],{},"authorized_keys"," に登録してください。",[16,537,538],{},"認証情報の保存を明示的に有効にした場合、TunnelDeck は OS の安全なストアを利用します。",[97,540,541,544,547],{},[100,542,543],{},"macOS Keychain；",[100,545,546],{},"Windows Credential Manager；",[100,548,549],{},"Linux Secret Service。",[16,551,552,553,556],{},"通常の ",[25,554,555],{},"profiles.json"," にパスワード、秘密鍵の内容、鍵のパスフレーズは保存されません。それでもサーバー側では最小権限、接続元制限、鍵のローテーションを行ってください。",[81,558,560],{"id":559},"_5-初回接続でホスト指紋を確認する","5. 初回接続でホスト指紋を確認する",[16,562,563],{},"初めて SSH サーバーへ接続すると、TunnelDeck は SHA-256 ホスト指紋を表示します。ダイアログが出たという理由だけで承認せず、サーバーコンソール、運用記録、別の信頼できる経路と照合してから「信頼して接続」を選びます。",[16,565,566,567,570],{},"確認した鍵は TunnelDeck 専用の ",[25,568,569],{},"known_hosts"," に保存されます。後からサーバー鍵が変わると、TunnelDeck は新しい鍵を暗黙に受け入れず、接続を停止します。正規のサーバー再構築でも変わることはありますが、信頼情報を更新する前に原因を確認してください。ホスト名の誤りや中間者攻撃の可能性もあります。",[81,572,574],{"id":573},"_6-接続後にサービスを利用する","6. 接続後にサービスを利用する",[16,576,577],{},"トンネルが「実行中」になったら、クライアントをローカル入口へ向けます。",[97,579,580,587,593],{},[100,581,582,583,586],{},"Web アプリ：",[25,584,585],{},"http:\u002F\u002F127.0.0.1:9108"," を開く；",[100,588,589,590,592],{},"データベース：クライアントの接続先を ",[25,591,218],{}," と設定したローカルポートにする；",[100,594,595],{},"その他の TCP：各クライアントで同じローカル入口を指定。",[16,597,598],{},"HTTP／HTTPS の場合は「これは Web サービス」を有効にし、プロトコルを選択できます。トンネルの実行中に「Web ページを開く」ボタンが表示され、ユーザーが選択したときだけ既定ブラウザーを起動します。",[16,600,601,602,605],{},"アプリ起動時、トンネル開始時、自動再接続時に TunnelDeck が勝手にページを開くことは",[20,603,604],{},"ありません","。転送対象の多くは HTTP ではないため、接続とブラウザー起動を分けることで、無効な URL や繰り返し開くウィンドウを防ぎます。",[81,607,609],{"id":608},"_7-日常管理と自動再接続","7. 日常管理と自動再接続",[16,611,612],{},"プロファイル一覧には状態とアクティブ接続数が表示されます。開始、停止、編集、削除を必要に応じて行えます。実行中の設定を変更するときは古いトンネルを停止してから反映し、古いリスナーを新設定と取り違えないようにします。",[16,614,615],{},"TunnelDeck は SSH keepalive を送り、一時的な切断には 2〜30 秒の指数バックオフで再接続します。再接続はトンネルを復元するだけで、ブラウザーは開きません。",[81,617,618],{"id":618},"トラブルシューティング",[248,620,621],{"id":621},"ローカルポートを開けない",[16,623,624],{},"別プロセスが使用している可能性があります。ローカルポートを変更するか、使用中のプロセスを確認します。",[33,626,628],{"className":35,"code":627,"language":37,"meta":38,"style":38},"lsof -nP -iTCP:9108 -sTCP:LISTEN\n",[25,629,630],{"__ignoreMap":38},[42,631,632,635,638,641],{"class":44,"line":45},[42,633,634],{"class":48},"lsof",[42,636,637],{"class":52}," -nP",[42,639,640],{"class":52}," -iTCP:9108",[42,642,643],{"class":52}," -sTCP:LISTEN\n",[248,645,647],{"id":646},"ssh-サーバーへ到達できない","SSH サーバーへ到達できない",[16,649,650],{},"ホスト名、SSH ポート、ユーザー名、ファイアウォール、現在のネットワークから接続が許可されているかを確認します。同じ非機密パラメーターでターミナルから接続すると切り分けに役立ちますが、パスワードをコマンドやスクリーンショットへ入れないでください。",[248,652,653],{"id":653},"トンネルは動くが転送先が応答しない",[16,655,656,657,659],{},"SSH サーバーの視点で転送先を確認します。",[25,658,218],{}," を指定したなら、サービスは SSH サーバー自身で動いている必要があります。対象ポートで実際に待ち受けているか、SSH ホストからの接続を許可しているかも確認します。",[248,661,662],{"id":662},"秘密鍵が使えない",[16,664,665,668],{},[25,666,667],{},".pub"," ではなく秘密鍵を選んだか、鍵形式が対応しているか、暗号化鍵のパスフレーズが正しいかを確認します。秘密鍵は現在の OS ユーザーだけが読める権限にしてください。",[248,670,671],{"id":671},"ホスト鍵変更の警告が出る",[16,673,674],{},"接続を止め、サーバーが再構築されたか、ホスト名の向き先が変わったかを確認します。信頼できる経路で新しい指紋を照合した場合だけ登録を更新してください。警告を消すためだけに検証を無効化してはいけません。",[81,676,677],{"id":677},"デスクトップ版が軽量な理由",[16,679,680,681,684],{},"TunnelDeck は Go、Wails、Vue 3 で構築されています。Chromium 一式を同梱せず OS の WebView を利用し、トンネルは ",[25,682,683],{},"golang.org\u002Fx\u002Fcrypto\u002Fssh"," から直接確立します。現在の macOS ARM64 アプリバンドルは約 8.7 MB です。",[16,686,687,688,693,694,699],{},"MIT License のオープンソースで、ソース、インストーラー、リリース履歴は ",[387,689,692],{"href":690,"rel":691},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck",[391],"TunnelDeck GitHub リポジトリ","から確認できます。現在の安定版ソースは ",[387,695,698],{"href":696,"rel":697},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002FTunnelDeck\u002Freleases\u002Ftag\u002Fv0.3.1",[391],"v0.3.1","です。",[81,701,703],{"id":702},"chrome-ウェブストア版を公開しました","Chrome ウェブストア版を公開しました",[16,705,706,711,712,715],{},[387,707,710],{"href":708,"rel":709},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Ftunneldeck\u002Fjnfkjehpbkmfnidfcilehhkpbjjinmod",[391],"TunnelDeck Chrome サイドパネル拡張 v0.3.1","は Chrome ウェブストアで一般公開され、ストアから直接インストールできるようになりました。ソースコード、デスクトップ版のインストールスクリプト、問題報告は引き続き ",[387,713,692],{"href":690,"rel":714},[391],"で管理しています。",[16,717,718],{},"次の順序でインストールしてください。",[408,720,721,724,727,734,737],{},[100,722,723],{},"この記事の第 1 節または GitHub README に従って TunnelDeck デスクトップ版をインストール；",[100,725,726],{},"Chrome ウェブストアから拡張機能をインストール；",[100,728,729,730,733],{},"デスクトップ版を起動し、「Chrome ブラウザー統合」で正式な拡張 ID ",[25,731,732],{},"jnfkjehpbkmfnidfcilehhkpbjjinmod"," を確認；",[100,735,736],{},"未登録と表示された場合は「Chrome サービスを登録」を選び、拡張機能を再読み込み；",[100,738,739],{},"Chrome ツールバーの TunnelDeck アイコンからサイドパネルを開いて接続を管理。",[16,741,742],{},"拡張機能は Native Messaging を通じて同じ PC の TunnelDeck を操作します。接続の作成、編集、インポート、開始、停止に対応し、Web サービスとして指定した接続もユーザーが操作したときだけ開きます。ブラウザープロセス内で SSH を確立するものではなく、ストア拡張は操作を便利にし、実際の SSH トンネルはデスクトップ版が実行します。",[744,745,746],"style",{},"html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}",{"title":38,"searchDepth":748,"depth":748,"links":749},3,[750,751,752,756,757,758,762,763,764,765,772,773],{"id":83,"depth":276,"text":84},{"id":135,"depth":276,"text":136},{"id":238,"depth":276,"text":239,"children":753},[754,755],{"id":250,"depth":748,"text":251},{"id":341,"depth":748,"text":342},{"id":402,"depth":276,"text":403},{"id":445,"depth":276,"text":446},{"id":518,"depth":276,"text":519,"children":759},[760,761],{"id":522,"depth":748,"text":522},{"id":528,"depth":748,"text":528},{"id":559,"depth":276,"text":560},{"id":573,"depth":276,"text":574},{"id":608,"depth":276,"text":609},{"id":618,"depth":276,"text":618,"children":766},[767,768,769,770,771],{"id":621,"depth":748,"text":621},{"id":646,"depth":748,"text":647},{"id":653,"depth":748,"text":653},{"id":662,"depth":748,"text":662},{"id":671,"depth":748,"text":671},{"id":677,"depth":276,"text":677},{"id":702,"depth":276,"text":703},"TunnelDeck をソースから導入し、SSH ローカル転送の作成・インポート、パスワードまたは鍵認証、ホスト指紋の確認、Web サービスの安全な起動まで解説します。",false,null,"md",true,{"src":780,"alt":781,"caption":782,"width":783,"height":784},"\u002Fmedia\u002Fposts\u002Ftunneldeck-desktop-ssh-tunnel-guide\u002Ftunneldeck-macos-v1.webp","SSH サーバー、ローカル入口、転送先を設定する TunnelDeck デスクトップ版の新規トンネル画面","TunnelDeck v0.3.1 の実画面です。表示されているアドレスは例であり、実在するサーバーや認証情報は含まれません。画像を選択すると全画面で確認できます。",2360,1624,{},"\u002Fblog\u002Ftunneldeck-desktop-ssh-tunnel-guide","2026-08-07",13,{"title":10,"description":774},"blog\u002Ftunneldeck-desktop-ssh-tunnel-guide",[22,792,793,794,795],"SSH","ポート転送","オープンソース","Wails","2026-08-10","lCiM14gxnG_aGLcQhMzAM-ILiPbDGT2LCbFfRMgo1nU",[799,1297,3320,4932,6532,8929,10638,10896,11349,11751],{"id":9,"title":10,"accent":11,"body":800,"description":774,"draft":775,"experience":776,"extension":777,"featured":778,"image":1293,"meta":1294,"navigation":778,"path":786,"publishedAt":787,"readingTime":788,"seo":1295,"stem":790,"tags":1296,"updatedAt":796,"video":776,"__hash__":797,"contentLocale":6,"isFallback":775},{"type":13,"value":801,"toc":1267},[802,808,810,828,834,836,838,844,858,866,868,870,924,932,937,939,943,945,947,973,975,991,995,1011,1013,1021,1025,1039,1047,1049,1051,1065,1075,1077,1079,1113,1121,1123,1125,1127,1129,1133,1135,1143,1147,1149,1151,1155,1157,1159,1171,1173,1177,1179,1181,1183,1185,1187,1189,1203,1205,1207,1209,1213,1215,1219,1221,1223,1225,1229,1237,1239,1247,1249,1263,1265],[16,803,18,804,23,806,28],{},[20,805,22],{},[25,807,27],{},[16,809,31],{},[33,811,812],{"className":35,"code":36,"language":37,"meta":38,"style":38},[25,813,814],{"__ignoreMap":38},[42,815,816,818,820,822,824,826],{"class":44,"line":45},[42,817,49],{"class":48},[42,819,53],{"class":52},[42,821,57],{"class":56},[42,823,60],{"class":52},[42,825,63],{"class":52},[42,827,66],{"class":56},[16,829,69,830,73,832,76],{},[25,831,72],{},[25,833,72],{},[16,835,79],{},[81,837,84],{"id":83},[16,839,87,840,95],{},[20,841,90,842,94],{},[25,843,93],{},[97,845,846,848,850,852,856],{},[100,847,102],{},[100,849,105],{},[100,851,108],{},[100,853,111,854,114],{},[25,855,27],{},[100,857,117],{},[16,859,120,860,124,862,128,864,132],{},[25,861,123],{},[25,863,127],{},[25,865,131],{},[81,867,136],{"id":135},[16,869,139],{},[141,871,872,882],{},[144,873,874],{},[147,875,876,878,880],{},[150,877,152],{},[150,879,155],{},[150,881,158],{},[160,883,884,894,904,914],{},[147,885,886,888,892],{},[165,887,167],{},[165,889,890],{},[25,891,172],{},[165,893,175],{},[147,895,896,898,902],{},[165,897,180],{},[165,899,900],{},[25,901,185],{},[165,903,188],{},[147,905,906,908,912],{},[165,907,193],{},[165,909,910],{},[25,911,72],{},[165,913,200],{},[147,915,916,918,922],{},[165,917,205],{},[165,919,920],{},[25,921,72],{},[165,923,212],{},[16,925,215,926,219,928,223,930,227],{},[25,927,218],{},[20,929,222],{},[25,931,226],{},[33,933,935],{"className":934,"code":232,"language":233,"meta":38},[231],[25,936,232],{"__ignoreMap":38},[81,938,239],{"id":238},[16,940,242,941,246],{},[20,942,245],{},[248,944,251],{"id":250},[16,946,254],{},[33,948,949],{"className":35,"code":257,"language":37,"meta":38,"style":38},[25,950,951,961],{"__ignoreMap":38},[42,952,953,955,957,959],{"class":44,"line":45},[42,954,264],{"class":48},[42,956,267],{"class":52},[42,958,270],{"class":56},[42,960,273],{"class":52},[42,962,963,965,967,969,971],{"class":44,"line":276},[42,964,280],{"class":279},[42,966,283],{"class":48},[42,968,286],{"class":52},[42,970,289],{"class":52},[42,972,292],{"class":52},[16,974,295],{},[33,976,977],{"className":35,"code":298,"language":37,"meta":38,"style":38},[25,978,979],{"__ignoreMap":38},[42,980,981,983,985,987,989],{"class":44,"line":45},[42,982,264],{"class":48},[42,984,267],{"class":52},[42,986,270],{"class":56},[42,988,311],{"class":279},[42,990,314],{"class":48},[16,992,993,319],{},[25,994,264],{},[33,996,997],{"className":35,"code":322,"language":37,"meta":38,"style":38},[25,998,999],{"__ignoreMap":38},[42,1000,1001,1003,1005,1007,1009],{"class":44,"line":45},[42,1002,329],{"class":48},[42,1004,332],{"class":52},[42,1006,270],{"class":56},[42,1008,311],{"class":279},[42,1010,314],{"class":48},[248,1012,342],{"id":341},[33,1014,1015],{"className":345,"code":346,"language":347,"meta":38,"style":38},[25,1016,1017],{"__ignoreMap":38},[42,1018,1019],{"class":44,"line":45},[42,1020,346],{},[16,1022,356,1023,360],{},[25,1024,359],{},[97,1026,1027,1031,1035],{},[100,1028,365,1029,369],{},[25,1030,368],{},[100,1032,372,1033,369],{},[25,1034,375],{},[100,1036,378,1037,382],{},[25,1038,381],{},[16,1040,385,1041,393,1044,399],{},[387,1042,392],{"href":389,"rel":1043},[391],[387,1045,398],{"href":396,"rel":1046},[391],[81,1048,403],{"id":402},[16,1050,406],{},[408,1052,1053,1055,1059,1061,1063],{},[100,1054,412],{},[100,1056,415,1057,418],{},[25,1058,27],{},[100,1060,421],{},[100,1062,424],{},[100,1064,427],{},[16,1066,430,1067,434,1069,437,1071,437,1073,442],{},[20,1068,433],{},[25,1070,123],{},[25,1072,127],{},[25,1074,131],{},[81,1076,446],{"id":445},[16,1078,449],{},[408,1080,1081,1085,1093,1097,1103,1107,1111],{},[100,1082,1083,457],{},[20,1084,456],{},[100,1086,1087,463,1089,467,1091,471],{},[20,1088,462],{},[25,1090,466],{},[25,1092,470],{},[100,1094,1095,477],{},[20,1096,476],{},[100,1098,1099,483,1101,486],{},[20,1100,482],{},[25,1102,218],{},[100,1104,1105,492],{},[20,1106,491],{},[100,1108,1109,498],{},[20,1110,497],{},[100,1112,501],{},[16,1114,504,1115,507,1117,511,1119,515],{},[25,1116,218],{},[25,1118,510],{},[25,1120,514],{},[81,1122,519],{"id":518},[248,1124,522],{"id":522},[16,1126,525],{},[248,1128,528],{"id":528},[16,1130,531,1131,535],{},[25,1132,534],{},[16,1134,538],{},[97,1136,1137,1139,1141],{},[100,1138,543],{},[100,1140,546],{},[100,1142,549],{},[16,1144,552,1145,556],{},[25,1146,555],{},[81,1148,560],{"id":559},[16,1150,563],{},[16,1152,566,1153,570],{},[25,1154,569],{},[81,1156,574],{"id":573},[16,1158,577],{},[97,1160,1161,1165,1169],{},[100,1162,582,1163,586],{},[25,1164,585],{},[100,1166,589,1167,592],{},[25,1168,218],{},[100,1170,595],{},[16,1172,598],{},[16,1174,601,1175,605],{},[20,1176,604],{},[81,1178,609],{"id":608},[16,1180,612],{},[16,1182,615],{},[81,1184,618],{"id":618},[248,1186,621],{"id":621},[16,1188,624],{},[33,1190,1191],{"className":35,"code":627,"language":37,"meta":38,"style":38},[25,1192,1193],{"__ignoreMap":38},[42,1194,1195,1197,1199,1201],{"class":44,"line":45},[42,1196,634],{"class":48},[42,1198,637],{"class":52},[42,1200,640],{"class":52},[42,1202,643],{"class":52},[248,1204,647],{"id":646},[16,1206,650],{},[248,1208,653],{"id":653},[16,1210,656,1211,659],{},[25,1212,218],{},[248,1214,662],{"id":662},[16,1216,1217,668],{},[25,1218,667],{},[248,1220,671],{"id":671},[16,1222,674],{},[81,1224,677],{"id":677},[16,1226,680,1227,684],{},[25,1228,683],{},[16,1230,687,1231,693,1234,699],{},[387,1232,692],{"href":690,"rel":1233},[391],[387,1235,698],{"href":696,"rel":1236},[391],[81,1238,703],{"id":702},[16,1240,1241,711,1244,715],{},[387,1242,710],{"href":708,"rel":1243},[391],[387,1245,692],{"href":690,"rel":1246},[391],[16,1248,718],{},[408,1250,1251,1253,1255,1259,1261],{},[100,1252,723],{},[100,1254,726],{},[100,1256,729,1257,733],{},[25,1258,732],{},[100,1260,736],{},[100,1262,739],{},[16,1264,742],{},[744,1266,746],{},{"title":38,"searchDepth":748,"depth":748,"links":1268},[1269,1270,1271,1275,1276,1277,1281,1282,1283,1284,1291,1292],{"id":83,"depth":276,"text":84},{"id":135,"depth":276,"text":136},{"id":238,"depth":276,"text":239,"children":1272},[1273,1274],{"id":250,"depth":748,"text":251},{"id":341,"depth":748,"text":342},{"id":402,"depth":276,"text":403},{"id":445,"depth":276,"text":446},{"id":518,"depth":276,"text":519,"children":1278},[1279,1280],{"id":522,"depth":748,"text":522},{"id":528,"depth":748,"text":528},{"id":559,"depth":276,"text":560},{"id":573,"depth":276,"text":574},{"id":608,"depth":276,"text":609},{"id":618,"depth":276,"text":618,"children":1285},[1286,1287,1288,1289,1290],{"id":621,"depth":748,"text":621},{"id":646,"depth":748,"text":647},{"id":653,"depth":748,"text":653},{"id":662,"depth":748,"text":662},{"id":671,"depth":748,"text":671},{"id":677,"depth":276,"text":677},{"id":702,"depth":276,"text":703},{"src":780,"alt":781,"caption":782,"width":783,"height":784},{},{"title":10,"description":774},[22,792,793,794,795],{"id":1298,"title":1299,"accent":1300,"body":1301,"description":3292,"draft":775,"experience":776,"extension":777,"featured":775,"image":3293,"meta":3298,"navigation":778,"path":3299,"publishedAt":3300,"readingTime":3301,"seo":3302,"stem":3303,"tags":3304,"updatedAt":3311,"video":3312,"__hash__":3319,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fstundeck-docker-deployment-guide.md","StunDeck 実践：fnOS FPK、Docker、Cloudflare 302","cyan",{"type":13,"value":1302,"toc":3265},[1303,1314,1321,1324,1354,1360,1364,1373,1387,1411,1414,1438,1445,1466,1469,1475,1481,1484,1494,1497,1501,1521,1529,1532,1536,1549,1763,1773,1810,1813,1820,1911,1918,1984,1991,1998,2004,2008,2035,2041,2288,2302,2317,2321,2384,2391,2447,2454,2460,2465,2469,2476,2493,2499,2504,2510,2518,2521,2525,2528,2535,2541,2544,2550,2556,2614,2624,2630,2633,2636,2642,2645,2649,2656,2670,2676,2681,2687,2691,2694,2700,2706,2711,2715,2754,2757,2764,2767,2771,2774,2780,2787,2813,2819,2825,2829,2832,2869,2878,2884,2889,2892,2931,2941,2947,2953,2957,2964,2990,2994,3014,3018,3041,3045,3048,3052,3058,3088,3091,3136,3139,3155,3158,3177,3183,3186,3255,3262],[16,1304,1305,1306,1309,1310,1313],{},"この記事の中心は、STUN を単独で動かすことではありません。利用者は常に一つの",[20,1307,1308],{},"固定ドメイン","へアクセスし、StunDeck が公開 IP または動的ポートの変化を検出したら Cloudflare Single Redirect を更新します。Cloudflare は ",[20,1311,1312],{},"302","（または 307）を返し、クライアントは現在の公開マッピングへ移動します。",[16,1315,1316,1317,1320],{},"導入は Docker から始めますが、順番が重要です。",[20,1318,1319],{},"StunDeck の導入 → Cloudflare Token の作成 → Cloudflare 接続の保存 → Redirect サービスの作成 → STUN\u002FUPnP の起動 → 第一段と第二段の検証","の順に進めます。サービス作成前に Cloudflare を設定しないと、接続を選択できず、自動同期も検証できません。",[16,1322,1323],{},"実測結果は次のとおりです。",[97,1325,1326,1329,1332,1339,1342,1345,1351],{},[100,1327,1328],{},"Debian 13.6、amd64；",[100,1330,1331],{},"Docker Engine 29.7.1、Docker Compose 5.3.1；",[100,1333,1334,1335,1338],{},"StunDeck イメージは公開コミット ",[25,1336,1337],{},"bcb5a1a"," に対応；",[100,1340,1341],{},"Docker サービスに常駐プロキシはなく、コンテナ内の proxy 変数もすべて空；",[100,1343,1344],{},"NATMap がマッピングを取得し、UPnP が第一段の待受ポートを開放；",[100,1346,1347,1348,733],{},"別回線の独立ホストから動的エンドポイントへ接続し HTTP 200 を確認し、固定入口からは HTTP 302 と ",[25,1349,1350],{},"Location",[100,1352,1353],{},"実際の認証情報、Token、LAN アドレス、公開 IP、動的ポートは本文と画像に掲載していません。",[1355,1356,1357],"blockquote",{},[16,1358,1359],{},"STUN は中継ではありません。対称型 NAT や制限の強い CGNAT を公開入口へ変換することはできません。この記事は外部回線テストまで成功した環境の記録であり、すべての回線で同じ結果を保証するものではありません。",[81,1361,1363],{"id":1362},"fnos-に検証済み-fpk-をインストールする","fnOS に検証済み FPK をインストールする",[1355,1365,1366],{},[16,1367,1368,1369,1372],{},"ストア状況（2026-08-26）：StunDeck は fnOS アプリセンターの公開審査へ提出済みです。承認されて一般公開されるまでは、ここで提供する正式版 FPK を手動インストールできます。fnOS のパッケージ形式は ",[20,1370,1371],{},"FPK"," であり、Android APK ではありません。",[16,1374,1375,1376,467,1379,1382,1383,1386],{},"現在の正式版は ",[25,1377,1378],{},"linux\u002Famd64",[25,1380,1381],{},"linux\u002Farm64"," に対応する ",[20,1384,1385],{},"StunDeck v0.1.1"," です。このブログに検証済みのコピーを置き、GitHub Release も予備の入手先として案内します。",[97,1388,1389,1397,1404],{},[100,1390,1391,1396],{},[387,1392,1395],{"href":1393,"rel":1394},"https:\u002F\u002Fblog.sparkles-editor.com\u002Fdownloads\u002Fstundeck\u002Fstundeck-fpk-0.1.1.fpk",[391],"このサイトから stundeck-fpk-0.1.1.fpk をダウンロード","（136,791 bytes）；",[100,1398,1399,471],{},[387,1400,1403],{"href":1401,"rel":1402},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck\u002Freleases\u002Ftag\u002Fv0.1.1",[391],"StunDeck v0.1.1 GitHub Release",[100,1405,1406,1407,1410],{},"SHA-256：",[25,1408,1409],{},"63d7f0e00b3777400507e62033fb0cce72884d10ad824f51b435a502d3a5dd8f","。",[16,1412,1413],{},"インストール手順：",[408,1415,1416,1419,1422,1428,1435],{},[100,1417,1418],{},"FPK をダウンロードし、ローカルで SHA-256 を照合します。",[100,1420,1421],{},"fnOS の「アプリセンター → 設定 → アプリを手動インストール」を開きます。",[100,1423,1424,1427],{},[25,1425,1426],{},"stundeck-fpk-0.1.1.fpk"," を選択してタイムゾーンを設定します。Secure Cookie は HTTPS リバースプロキシ経由で利用する場合だけ有効にします。",[100,1429,1430,1431,1434],{},"初回インストール時に ",[25,1432,1433],{},"20000–59999"," から未使用ポートが一つ選ばれて保存され、アップグレードや再起動で変更されません。",[100,1436,1437],{},"fnOS デスクトップから StunDeck を開き、管理者を作成してから Cloudflare Token、マッピングサービス、Webhook を追加します。",[16,1439,1440,1441,1444],{},"FPK は不変タグのイメージ ",[25,1442,1443],{},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:v0.1.1"," を取得するため、初回インストール時に NAS から GHCR へ接続できる必要があります。Cloudflare Token、Webhook Secret などの秘密情報がインストーラーへ同梱されたり、収集されたりすることはありません。",[16,1446,1447,1448,1451,1452,467,1455,1458,1459,1462,1463,1465],{},"設定と永続化ディレクトリは fnOS が管理し、実データは ",[25,1449,1450],{},"${TRIM_PKGVAR}\u002Fdata"," に保存されます。アップグレード、移行、再インストールの前に ",[25,1453,1454],{},"stundeck.db",[25,1456,1457],{},"master.key"," を",[20,1460,1461],{},"必ず一組でバックアップ","してください。",[25,1464,1457],{}," がなければ、データベース内の暗号化された Cloudflare Token、Webhook Secret、TOTP Secret は復元できません。アンインストール時は標準でデータを保持し、明示的に「完全削除」を選んだ場合だけ削除します。",[81,1467,1468],{"id":1468},"まず中心となる通信経路を理解する",[33,1470,1473],{"className":1471,"code":1472,"language":233,"meta":38},[231],"クライアントは常に固定ドメインへアクセス\n  └─ Cloudflare プロキシ DNS（オレンジ色の雲）\n       └─ Single Redirect が 302 \u002F 307 + Location を返す\n            └─ 現在の公開 IP:動的ポート\n                 └─ ルーターの UPnP \u002F NAT-PMP \u002F 手動転送\n                      └─ StunDeck + NATMap\n                           └─ LAN 内ターゲット\n",[25,1474,1472],{"__ignoreMap":38},[16,1476,1477,1478,1480],{},"固定されるのは入口ドメインであり、公開エンドポイントではありません。Cloudflare が処理するのは第一段だけです。第二段には Tunnel、中継、WAF、Access は適用されず、ブラウザは ",[25,1479,1350],{}," にある公開 IP と動的ポートへ直接接続します。",[16,1482,1483],{},"成功条件は二つに分けて確認します。",[408,1485,1486,1489],{},[100,1487,1488],{},"固定ドメインが正しい 302\u002F307 を返し、DNS と Single Redirect が同期していること；",[100,1490,1491,1493],{},[25,1492,1350],{}," をたどった先が正常応答し、STUN、ゲートウェイ開放、ファイアウォール、LAN 内サービスが機能していること。",[16,1495,1496],{},"第一段だけ成功しても、サービスが外部から利用可能とは限りません。",[81,1498,1500],{"id":1499},"_1-前提条件","1. 前提条件",[97,1502,1503,1506,1509,1512,1515,1518],{},[100,1504,1505],{},"Debian 13\u002F12 または Docker が公式サポートする Linux；",[100,1507,1508],{},"amd64 または arm64；",[100,1510,1511],{},"識別可能な LAN アドレス；",[100,1513,1514],{},"LAN 内ターゲットへ接続できること；",[100,1516,1517],{},"必要に応じて UPnP または手動ポート転送を設定できるルーター；",[100,1519,1520],{},"機密データを持たず、すぐ停止できるテストサービス。",[16,1522,1523,1528],{},[387,1524,1527],{"href":1525,"rel":1526},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fstun",[391],"Lucky の公式 STUN ドキュメント","は NAT1 を対象とし、LAN ホストでは UPnP、ポート転送、DMZ などが追加で必要だと説明しています。NAT タイプの表示名は製品ごとに違うため、ログと実際の外部回線テストを最終根拠にします。",[16,1530,1531],{},"NAS、ルーター、StunDeck 自体の管理画面を最初の公開テストに使わないでください。静的ページや一時 HTTP サービスから始めます。",[81,1533,1535],{"id":1534},"_2-docker-engine-と-compose-を導入する","2. Docker Engine と Compose を導入する",[16,1537,1538,1539,1544,1545,1548],{},"以下は Docker の",[387,1540,1543],{"href":1541,"rel":1542},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Finstall\u002Fdebian\u002F",[391],"Debian 公式手順","に沿っています。Compose は旧式の単体バイナリではなく ",[25,1546,1547],{},"docker-compose-plugin"," を使います。",[33,1550,1552],{"className":35,"code":1551,"language":37,"meta":38,"style":38},"sudo apt update\nsudo apt install -y ca-certificates curl\n\nsudo install -m 0755 -d \u002Fetc\u002Fapt\u002Fkeyrings\nsudo curl -fsSL https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg \\\n  -o \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\nsudo chmod a+r \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n\nsudo tee \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fdocker.sources >\u002Fdev\u002Fnull \u003C\u003C'EOF'\nTypes: deb\nURIs: https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\nSuites: trixie\nComponents: stable\nArchitectures: amd64\nSigned-By: \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\nEOF\n\nsudo apt update\nsudo apt install -y \\\n  docker-ce docker-ce-cli containerd.io \\\n  docker-buildx-plugin docker-compose-plugin\n",[25,1553,1554,1565,1583,1588,1607,1622,1631,1644,1649,1672,1678,1684,1690,1695,1701,1707,1713,1718,1727,1740,1754],{"__ignoreMap":38},[42,1555,1556,1559,1562],{"class":44,"line":45},[42,1557,1558],{"class":48},"sudo",[42,1560,1561],{"class":56}," apt",[42,1563,1564],{"class":56}," update\n",[42,1566,1567,1569,1571,1574,1577,1580],{"class":44,"line":276},[42,1568,1558],{"class":48},[42,1570,1561],{"class":56},[42,1572,1573],{"class":56}," install",[42,1575,1576],{"class":52}," -y",[42,1578,1579],{"class":56}," ca-certificates",[42,1581,1582],{"class":56}," curl\n",[42,1584,1585],{"class":44,"line":748},[42,1586,1587],{"emptyLinePlaceholder":778},"\n",[42,1589,1591,1593,1595,1598,1601,1604],{"class":44,"line":1590},4,[42,1592,1558],{"class":48},[42,1594,1573],{"class":56},[42,1596,1597],{"class":52}," -m",[42,1599,1600],{"class":52}," 0755",[42,1602,1603],{"class":52}," -d",[42,1605,1606],{"class":56}," \u002Fetc\u002Fapt\u002Fkeyrings\n",[42,1608,1610,1612,1615,1617,1620],{"class":44,"line":1609},5,[42,1611,1558],{"class":48},[42,1613,1614],{"class":56}," curl",[42,1616,267],{"class":52},[42,1618,1619],{"class":56}," https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[42,1621,273],{"class":52},[42,1623,1625,1628],{"class":44,"line":1624},6,[42,1626,1627],{"class":52},"  -o",[42,1629,1630],{"class":56}," \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[42,1632,1634,1636,1639,1642],{"class":44,"line":1633},7,[42,1635,1558],{"class":48},[42,1637,1638],{"class":56}," chmod",[42,1640,1641],{"class":56}," a+r",[42,1643,1630],{"class":56},[42,1645,1647],{"class":44,"line":1646},8,[42,1648,1587],{"emptyLinePlaceholder":778},[42,1650,1652,1654,1657,1660,1663,1666,1669],{"class":44,"line":1651},9,[42,1653,1558],{"class":48},[42,1655,1656],{"class":56}," tee",[42,1658,1659],{"class":56}," \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fdocker.sources",[42,1661,1662],{"class":279}," >",[42,1664,1665],{"class":56},"\u002Fdev\u002Fnull",[42,1667,1668],{"class":279}," \u003C\u003C",[42,1670,1671],{"class":56},"'EOF'\n",[42,1673,1675],{"class":44,"line":1674},10,[42,1676,1677],{"class":56},"Types: deb\n",[42,1679,1681],{"class":44,"line":1680},11,[42,1682,1683],{"class":56},"URIs: https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\n",[42,1685,1687],{"class":44,"line":1686},12,[42,1688,1689],{"class":56},"Suites: trixie\n",[42,1691,1692],{"class":44,"line":788},[42,1693,1694],{"class":56},"Components: stable\n",[42,1696,1698],{"class":44,"line":1697},14,[42,1699,1700],{"class":56},"Architectures: amd64\n",[42,1702,1704],{"class":44,"line":1703},15,[42,1705,1706],{"class":56},"Signed-By: \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n",[42,1708,1710],{"class":44,"line":1709},16,[42,1711,1712],{"class":56},"EOF\n",[42,1714,1716],{"class":44,"line":1715},17,[42,1717,1587],{"emptyLinePlaceholder":778},[42,1719,1721,1723,1725],{"class":44,"line":1720},18,[42,1722,1558],{"class":48},[42,1724,1561],{"class":56},[42,1726,1564],{"class":56},[42,1728,1730,1732,1734,1736,1738],{"class":44,"line":1729},19,[42,1731,1558],{"class":48},[42,1733,1561],{"class":56},[42,1735,1573],{"class":56},[42,1737,1576],{"class":52},[42,1739,273],{"class":52},[42,1741,1743,1746,1749,1752],{"class":44,"line":1742},20,[42,1744,1745],{"class":56},"  docker-ce",[42,1747,1748],{"class":56}," docker-ce-cli",[42,1750,1751],{"class":56}," containerd.io",[42,1753,273],{"class":52},[42,1755,1757,1760],{"class":44,"line":1756},21,[42,1758,1759],{"class":56},"  docker-buildx-plugin",[42,1761,1762],{"class":56}," docker-compose-plugin\n",[16,1764,1765,1766,467,1769,1772],{},"環境が異なる場合は確認して ",[25,1767,1768],{},"trixie",[25,1770,1771],{},"amd64"," を変更します。",[33,1774,1776],{"className":35,"code":1775,"language":37,"meta":38,"style":38},". \u002Fetc\u002Fos-release && echo \"$VERSION_CODENAME\"\ndpkg --print-architecture\n",[25,1777,1778,1802],{"__ignoreMap":38},[42,1779,1780,1783,1786,1790,1793,1796,1799],{"class":44,"line":45},[42,1781,1782],{"class":52},".",[42,1784,1785],{"class":56}," \u002Fetc\u002Fos-release",[42,1787,1789],{"class":1788},"suv1-"," && ",[42,1791,1792],{"class":52},"echo",[42,1794,1795],{"class":56}," \"",[42,1797,1798],{"class":1788},"$VERSION_CODENAME",[42,1800,1801],{"class":56},"\"\n",[42,1803,1804,1807],{"class":44,"line":276},[42,1805,1806],{"class":48},"dpkg",[42,1808,1809],{"class":52}," --print-architecture\n",[248,1811,1812],{"id":1812},"ダウンロード用プロキシは一時的に使う",[16,1814,1815,1816,1819],{},"今回の作業では Docker の GPG URL への直結が一度リセットされました。インストール時だけ使うプロキシを ",[25,1817,1818],{},"\u002Fetc\u002Fenvironment"," や Docker の systemd サービスへ残してはいけません。",[33,1821,1823],{"className":35,"code":1822,"language":37,"meta":38,"style":38},"PROXY_URL=http:\u002F\u002F192.168.50.9:7890\n\nsudo curl --proxy \"$PROXY_URL\" -fsSL \\\n  https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg \\\n  -o \u002Fetc\u002Fapt\u002Fkeyrings\u002Fdocker.asc\n\nsudo env http_proxy=\"$PROXY_URL\" https_proxy=\"$PROXY_URL\" apt update\nunset PROXY_URL\n",[25,1824,1825,1836,1840,1861,1868,1874,1878,1903],{"__ignoreMap":38},[42,1826,1827,1830,1833],{"class":44,"line":45},[42,1828,1829],{"class":1788},"PROXY_URL",[42,1831,1832],{"class":279},"=",[42,1834,1835],{"class":56},"http:\u002F\u002F192.168.50.9:7890\n",[42,1837,1838],{"class":44,"line":276},[42,1839,1587],{"emptyLinePlaceholder":778},[42,1841,1842,1844,1846,1849,1851,1854,1857,1859],{"class":44,"line":748},[42,1843,1558],{"class":48},[42,1845,1614],{"class":56},[42,1847,1848],{"class":52}," --proxy",[42,1850,1795],{"class":56},[42,1852,1853],{"class":1788},"$PROXY_URL",[42,1855,1856],{"class":56},"\"",[42,1858,267],{"class":52},[42,1860,273],{"class":52},[42,1862,1863,1866],{"class":44,"line":1590},[42,1864,1865],{"class":56},"  https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fdebian\u002Fgpg",[42,1867,273],{"class":52},[42,1869,1870,1872],{"class":44,"line":1609},[42,1871,1627],{"class":52},[42,1873,1630],{"class":56},[42,1875,1876],{"class":44,"line":1624},[42,1877,1587],{"emptyLinePlaceholder":778},[42,1879,1880,1882,1885,1888,1890,1892,1895,1897,1899,1901],{"class":44,"line":1633},[42,1881,1558],{"class":48},[42,1883,1884],{"class":56}," env",[42,1886,1887],{"class":56}," http_proxy=\"",[42,1889,1853],{"class":1788},[42,1891,1856],{"class":56},[42,1893,1894],{"class":56}," https_proxy=\"",[42,1896,1853],{"class":1788},[42,1898,1856],{"class":56},[42,1900,1561],{"class":56},[42,1902,1564],{"class":56},[42,1904,1905,1908],{"class":44,"line":1646},[42,1906,1907],{"class":52},"unset",[42,1909,1910],{"class":56}," PROXY_URL\n",[16,1912,1913,1914,1917],{},"公式リポジトリが不安定な場合は、Docker 公式手順の “Install from a package” を使い、対応する ",[25,1915,1916],{},".deb"," を検証してローカルインストールします。不明なミラーのバイナリは使いません。",[33,1919,1921],{"className":35,"code":1920,"language":37,"meta":38,"style":38},"sudo docker version\nsudo docker compose version\nsudo systemctl is-enabled docker\nsudo systemctl is-active docker\nsudo systemctl show docker -p Environment\n",[25,1922,1923,1933,1944,1957,1968],{"__ignoreMap":38},[42,1924,1925,1927,1930],{"class":44,"line":45},[42,1926,1558],{"class":48},[42,1928,1929],{"class":56}," docker",[42,1931,1932],{"class":56}," version\n",[42,1934,1935,1937,1939,1942],{"class":44,"line":276},[42,1936,1558],{"class":48},[42,1938,1929],{"class":56},[42,1940,1941],{"class":56}," compose",[42,1943,1932],{"class":56},[42,1945,1946,1948,1951,1954],{"class":44,"line":748},[42,1947,1558],{"class":48},[42,1949,1950],{"class":56}," systemctl",[42,1952,1953],{"class":56}," is-enabled",[42,1955,1956],{"class":56}," docker\n",[42,1958,1959,1961,1963,1966],{"class":44,"line":1590},[42,1960,1558],{"class":48},[42,1962,1950],{"class":56},[42,1964,1965],{"class":56}," is-active",[42,1967,1956],{"class":56},[42,1969,1970,1972,1974,1977,1979,1981],{"class":44,"line":1609},[42,1971,1558],{"class":48},[42,1973,1950],{"class":56},[42,1975,1976],{"class":56}," show",[42,1978,1929],{"class":56},[42,1980,60],{"class":52},[42,1982,1983],{"class":56}," Environment\n",[16,1985,1986,1987,1990],{},"今回の最終出力は ",[25,1988,1989],{},"Environment="," でした。コンテナ側は後でもう一度独立して確認します。",[16,1992,1993],{},[1994,1995],"img",{"alt":1996,"src":1997},"Docker Engine、Compose、systemd 状態の検証結果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fdocker-install-verified-v1.webp",[16,1999,2000],{},[2001,2002,2003],"em",{},"実際のコマンド出力を基に作成しました。ホスト名、ログイン情報、LAN アドレス、一時ダウンロードプロキシは削除済みです。",[81,2005,2007],{"id":2006},"_3-compose-ファイルを作る","3. Compose ファイルを作る",[33,2009,2011],{"className":35,"code":2010,"language":37,"meta":38,"style":38},"sudo install -d -m 0755 \u002Fopt\u002Fstundeck\ncd \u002Fopt\u002Fstundeck\n",[25,2012,2013,2028],{"__ignoreMap":38},[42,2014,2015,2017,2019,2021,2023,2025],{"class":44,"line":45},[42,2016,1558],{"class":48},[42,2018,1573],{"class":56},[42,2020,1603],{"class":52},[42,2022,1597],{"class":52},[42,2024,1600],{"class":52},[42,2026,2027],{"class":56}," \u002Fopt\u002Fstundeck\n",[42,2029,2030,2033],{"class":44,"line":276},[42,2031,2032],{"class":52},"cd",[42,2034,2027],{"class":56},[16,2036,2037,2040],{},[25,2038,2039],{},"\u002Fopt\u002Fstundeck\u002Fcompose.yaml","：",[33,2042,2046],{"className":2043,"code":2044,"language":2045,"meta":38,"style":38},"language-yaml shiki shiki-themes github-dark github-dark","services:\n  stundeck:\n    image: ghcr.io\u002Fnciae-zyh\u002Fstundeck:latest\n    network_mode: host\n    restart: unless-stopped\n    read_only: true\n    cap_drop: [ALL]\n    security_opt:\n      - no-new-privileges:true\n    tmpfs:\n      - \u002Ftmp:size=16m,mode=1777\n    volumes:\n      - stundeck-data:\u002Fvar\u002Flib\u002Fstundeck\n    environment:\n      STUNDECK_LISTEN: 0.0.0.0:8080\n      STUNDECK_DATA_DIR: \u002Fvar\u002Flib\u002Fstundeck\n      STUNDECK_SECURE_COOKIES: \"false\"\n      TZ: Asia\u002FShanghai\n      HTTP_PROXY: \"\"\n      HTTPS_PROXY: \"\"\n      ALL_PROXY: \"\"\n      http_proxy: \"\"\n      https_proxy: \"\"\n      all_proxy: \"\"\n\nvolumes:\n  stundeck-data:\n","yaml",[25,2047,2048,2057,2064,2075,2085,2095,2105,2119,2126,2134,2141,2148,2155,2162,2169,2179,2189,2199,2209,2219,2228,2237,2247,2257,2267,2272,2280],{"__ignoreMap":38},[42,2049,2050,2054],{"class":44,"line":45},[42,2051,2053],{"class":2052},"sxg3X","services",[42,2055,2056],{"class":1788},":\n",[42,2058,2059,2062],{"class":44,"line":276},[42,2060,2061],{"class":2052},"  stundeck",[42,2063,2056],{"class":1788},[42,2065,2066,2069,2072],{"class":44,"line":748},[42,2067,2068],{"class":2052},"    image",[42,2070,2071],{"class":1788},": ",[42,2073,2074],{"class":56},"ghcr.io\u002Fnciae-zyh\u002Fstundeck:latest\n",[42,2076,2077,2080,2082],{"class":44,"line":1590},[42,2078,2079],{"class":2052},"    network_mode",[42,2081,2071],{"class":1788},[42,2083,2084],{"class":56},"host\n",[42,2086,2087,2090,2092],{"class":44,"line":1609},[42,2088,2089],{"class":2052},"    restart",[42,2091,2071],{"class":1788},[42,2093,2094],{"class":56},"unless-stopped\n",[42,2096,2097,2100,2102],{"class":44,"line":1624},[42,2098,2099],{"class":2052},"    read_only",[42,2101,2071],{"class":1788},[42,2103,2104],{"class":52},"true\n",[42,2106,2107,2110,2113,2116],{"class":44,"line":1633},[42,2108,2109],{"class":2052},"    cap_drop",[42,2111,2112],{"class":1788},": [",[42,2114,2115],{"class":56},"ALL",[42,2117,2118],{"class":1788},"]\n",[42,2120,2121,2124],{"class":44,"line":1646},[42,2122,2123],{"class":2052},"    security_opt",[42,2125,2056],{"class":1788},[42,2127,2128,2131],{"class":44,"line":1651},[42,2129,2130],{"class":1788},"      - ",[42,2132,2133],{"class":56},"no-new-privileges:true\n",[42,2135,2136,2139],{"class":44,"line":1674},[42,2137,2138],{"class":2052},"    tmpfs",[42,2140,2056],{"class":1788},[42,2142,2143,2145],{"class":44,"line":1680},[42,2144,2130],{"class":1788},[42,2146,2147],{"class":56},"\u002Ftmp:size=16m,mode=1777\n",[42,2149,2150,2153],{"class":44,"line":1686},[42,2151,2152],{"class":2052},"    volumes",[42,2154,2056],{"class":1788},[42,2156,2157,2159],{"class":44,"line":788},[42,2158,2130],{"class":1788},[42,2160,2161],{"class":56},"stundeck-data:\u002Fvar\u002Flib\u002Fstundeck\n",[42,2163,2164,2167],{"class":44,"line":1697},[42,2165,2166],{"class":2052},"    environment",[42,2168,2056],{"class":1788},[42,2170,2171,2174,2176],{"class":44,"line":1703},[42,2172,2173],{"class":2052},"      STUNDECK_LISTEN",[42,2175,2071],{"class":1788},[42,2177,2178],{"class":56},"0.0.0.0:8080\n",[42,2180,2181,2184,2186],{"class":44,"line":1709},[42,2182,2183],{"class":2052},"      STUNDECK_DATA_DIR",[42,2185,2071],{"class":1788},[42,2187,2188],{"class":56},"\u002Fvar\u002Flib\u002Fstundeck\n",[42,2190,2191,2194,2196],{"class":44,"line":1715},[42,2192,2193],{"class":2052},"      STUNDECK_SECURE_COOKIES",[42,2195,2071],{"class":1788},[42,2197,2198],{"class":56},"\"false\"\n",[42,2200,2201,2204,2206],{"class":44,"line":1720},[42,2202,2203],{"class":2052},"      TZ",[42,2205,2071],{"class":1788},[42,2207,2208],{"class":56},"Asia\u002FShanghai\n",[42,2210,2211,2214,2216],{"class":44,"line":1729},[42,2212,2213],{"class":2052},"      HTTP_PROXY",[42,2215,2071],{"class":1788},[42,2217,2218],{"class":56},"\"\"\n",[42,2220,2221,2224,2226],{"class":44,"line":1742},[42,2222,2223],{"class":2052},"      HTTPS_PROXY",[42,2225,2071],{"class":1788},[42,2227,2218],{"class":56},[42,2229,2230,2233,2235],{"class":44,"line":1756},[42,2231,2232],{"class":2052},"      ALL_PROXY",[42,2234,2071],{"class":1788},[42,2236,2218],{"class":56},[42,2238,2240,2243,2245],{"class":44,"line":2239},22,[42,2241,2242],{"class":2052},"      http_proxy",[42,2244,2071],{"class":1788},[42,2246,2218],{"class":56},[42,2248,2250,2253,2255],{"class":44,"line":2249},23,[42,2251,2252],{"class":2052},"      https_proxy",[42,2254,2071],{"class":1788},[42,2256,2218],{"class":56},[42,2258,2260,2263,2265],{"class":44,"line":2259},24,[42,2261,2262],{"class":2052},"      all_proxy",[42,2264,2071],{"class":1788},[42,2266,2218],{"class":56},[42,2268,2270],{"class":44,"line":2269},25,[42,2271,1587],{"emptyLinePlaceholder":778},[42,2273,2275,2278],{"class":44,"line":2274},26,[42,2276,2277],{"class":2052},"volumes",[42,2279,2056],{"class":1788},[42,2281,2283,2286],{"class":44,"line":2282},27,[42,2284,2285],{"class":2052},"  stundeck-data",[42,2287,2056],{"class":1788},[16,2289,2290,2293,2294,2297,2298,2301],{},[25,2291,2292],{},"network_mode: host"," により NATMap と UPnP が実際の Linux ネットワークスタックを利用できます。コンテナは read-only、全 capability 削除、",[25,2295,2296],{},"no-new-privileges"," 有効で、永続化対象は ",[25,2299,2300],{},"\u002Fvar\u002Flib\u002Fstundeck"," だけです。proxy 変数は明示的に空へ固定します。",[16,2303,2304,2305,2308,2309,2312,2313,2316],{},"初期 LAN HTTP では ",[25,2306,2307],{},"STUNDECK_SECURE_COOKIES: \"false\"","、HTTPS リバースプロキシ配下では ",[25,2310,2311],{},"\"true\""," にします。Docker Desktop を正式な STUN ゲートウェイにせず、",[25,2314,2315],{},"privileged: true"," も追加しません。",[81,2318,2320],{"id":2319},"_4-起動と確認","4. 起動と確認",[33,2322,2324],{"className":35,"code":2323,"language":37,"meta":38,"style":38},"cd \u002Fopt\u002Fstundeck\nsudo docker compose config --quiet\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[25,2325,2326,2332,2346,2357,2373],{"__ignoreMap":38},[42,2327,2328,2330],{"class":44,"line":45},[42,2329,2032],{"class":52},[42,2331,2027],{"class":56},[42,2333,2334,2336,2338,2340,2343],{"class":44,"line":276},[42,2335,1558],{"class":48},[42,2337,1929],{"class":56},[42,2339,1941],{"class":56},[42,2341,2342],{"class":56}," config",[42,2344,2345],{"class":52}," --quiet\n",[42,2347,2348,2350,2352,2354],{"class":44,"line":748},[42,2349,1558],{"class":48},[42,2351,1929],{"class":56},[42,2353,1941],{"class":56},[42,2355,2356],{"class":56}," pull\n",[42,2358,2359,2361,2363,2365,2368,2370],{"class":44,"line":1590},[42,2360,1558],{"class":48},[42,2362,1929],{"class":56},[42,2364,1941],{"class":56},[42,2366,2367],{"class":56}," up",[42,2369,1603],{"class":52},[42,2371,2372],{"class":52}," --no-build\n",[42,2374,2375,2377,2379,2381],{"class":44,"line":1609},[42,2376,1558],{"class":48},[42,2378,1929],{"class":56},[42,2380,1941],{"class":56},[42,2382,2383],{"class":56}," ps\n",[16,2385,2386,2387,2390],{},"今回、約 10 秒で ",[25,2388,2389],{},"healthy"," になりました。",[33,2392,2394],{"className":35,"code":2393,"language":37,"meta":38,"style":38},"curl -fsS http:\u002F\u002F127.0.0.1:8080\u002Fapi\u002Fv1\u002Fhealth\n\nsudo docker inspect stundeck-stundeck-1 \\\n  --format '{{range .Config.Env}}{{println .}}{{end}}' \\\n  | grep -i proxy\n",[25,2395,2396,2406,2410,2424,2434],{"__ignoreMap":38},[42,2397,2398,2400,2403],{"class":44,"line":45},[42,2399,264],{"class":48},[42,2401,2402],{"class":52}," -fsS",[42,2404,2405],{"class":56}," http:\u002F\u002F127.0.0.1:8080\u002Fapi\u002Fv1\u002Fhealth\n",[42,2407,2408],{"class":44,"line":276},[42,2409,1587],{"emptyLinePlaceholder":778},[42,2411,2412,2414,2416,2419,2422],{"class":44,"line":748},[42,2413,1558],{"class":48},[42,2415,1929],{"class":56},[42,2417,2418],{"class":56}," inspect",[42,2420,2421],{"class":56}," stundeck-stundeck-1",[42,2423,273],{"class":52},[42,2425,2426,2429,2432],{"class":44,"line":1590},[42,2427,2428],{"class":52},"  --format",[42,2430,2431],{"class":56}," '{{range .Config.Env}}{{println .}}{{end}}'",[42,2433,273],{"class":52},[42,2435,2436,2438,2441,2444],{"class":44,"line":1609},[42,2437,280],{"class":279},[42,2439,2440],{"class":48}," grep",[42,2442,2443],{"class":52}," -i",[42,2445,2446],{"class":56}," proxy\n",[16,2448,2449,2450,2453],{},"health API は ",[25,2451,2452],{},"status: ok"," を返す必要があります。2 番目のコマンドに 6 個の proxy 変数が表示されても、値はすべて空でなければなりません。イメージ取得時の一時 daemon proxy は StunDeck 起動前に削除し、Docker を再起動します。",[16,2455,2456],{},[1994,2457],{"alt":2458,"src":2459},"Compose 起動、health API、空のコンテナ proxy 変数の検証結果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcompose-health-verified-v1.webp",[16,2461,2462],{},[2001,2463,2464],{},"実環境で healthy、health OK、コンテナ内 proxy 値なしを確認しました。ホストとコンテナの識別情報は削除しています。",[81,2466,2468],{"id":2467},"_5-初回セキュリティ設定","5. 初回セキュリティ設定",[16,2470,2471,2472,2475],{},"LAN から ",[25,2473,2474],{},"http:\u002F\u002FLAN_SERVER_IP:8080"," を開きます。",[408,2477,2478,2481,2484,2487,2490],{},[100,2479,2480],{},"一意の管理者名と 12 文字以上の強いパスワードを作る；",[100,2482,2483],{},"アクセスモードは「LAN」を選ぶ；",[100,2485,2486],{},"リバースプロキシ名が決まっている場合だけ許可 Host を設定；",[100,2488,2489],{},"Security 画面で Host allowlist と認証アプリ 2FA を設定；",[100,2491,2492],{},"8080 の管理画面を STUN で直接公開しない。",[16,2494,2495],{},[1994,2496],{"alt":2497,"src":2498},"空の StunDeck 初回管理者作成画面","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-first-run-v1.webp",[16,2500,2501],{},[2001,2502,2503],{},"使い捨ての空データ環境で撮影した実画面です。ユーザー名とパスワードは入力していません。",[16,2505,2506],{},[1994,2507],{"alt":2508,"src":2509},"LAN アクセス、許可 Host、2FA 入口を表示する StunDeck Security 画面","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-security-domain-v1.webp",[16,2511,2512],{},[2001,2513,2514,2517],{},[25,2515,2516],{},"stun-lab-20260802.sparkles-editor.com"," は公開許可済みの Host 例です。使い捨て環境へ入力しただけで保存していません。DNS レコード、Zone\u002FRule ID、Token、TOTP secret、管理者情報は含まれません。",[16,2519,2520],{},"遠隔管理には Cloudflare Tunnel、VPN、または認証付き HTTPS リバースプロキシを優先します。",[81,2522,2524],{"id":2523},"_6-cloudflare-の最小権限-api-token-を先に作る","6. Cloudflare の最小権限 API Token を先に作る",[16,2526,2527],{},"Single Redirect は Cloudflare を通過する通信だけを処理します。入口ドメインは対象 Zone に存在し、**プロキシ DNS（オレンジ色の雲）**である必要があります。DNS 値、Zone ID、Ruleset ID、他のドメインは画像へ掲載しません。",[16,2529,2530,2531,2534],{},"Cloudflare のユーザーメニューから ",[20,2532,2533],{},"プロフィール → API Token"," を開き、「Token を作成」を選びます。",[16,2536,2537],{},[1994,2538],{"alt":2539,"src":2540},"Cloudflare User API Tokens 画面の Token 作成入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-entry-v2.webp",[16,2542,2543],{},"「カスタム Token を作成」を選びます。Global API Key は使いません。",[16,2545,2546],{},[1994,2547],{"alt":2548,"src":2549},"Cloudflare Custom Token の作成入口","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-custom-token-v2.webp",[16,2551,2552,2555],{},[25,2553,2554],{},"StunDeck Redirect · sparkles-editor.com"," のような識別しやすい名前を付け、Zone 権限を次の 3 件だけ追加します。",[141,2557,2558,2574],{},[144,2559,2560],{},[147,2561,2562,2565,2568,2571],{},[150,2563,2564],{},"Resource",[150,2566,2567],{},"Permission",[150,2569,2570],{},"Level",[150,2572,2573],{},"用途",[160,2575,2576,2589,2602],{},[147,2577,2578,2581,2583,2586],{},[165,2579,2580],{},"Zone",[165,2582,2580],{},[165,2584,2585],{},"Read",[165,2587,2588],{},"対象 Zone の検索と検証",[147,2590,2591,2593,2596,2599],{},[165,2592,2580],{},[165,2594,2595],{},"DNS",[165,2597,2598],{},"Edit",[165,2600,2601],{},"プロキシ DNS レコードの作成・更新",[147,2603,2604,2606,2609,2611],{},[165,2605,2580],{},[165,2607,2608],{},"Single Redirect",[165,2610,2598],{},[165,2612,2613],{},"302\u002F307 Rule の作成・更新",[16,2615,2616,2617,2623],{},"Zone Resources は ",[20,2618,2619,2620],{},"Include → Specific zone → ",[25,2621,2622],{},"sparkles-editor.com"," に限定します。すべての Zone を許可しないでください。自動 DNS を使わない場合は DNS Edit を外し、入口レコードを自分でプロキシ状態に保ちます。",[16,2625,2626],{},[1994,2627],{"alt":2628,"src":2629},"1 Zone に限定した Cloudflare の 3 つの最小権限","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-permissions-v2.webp",[16,2631,2632],{},"Client IP フィルターは管理側の出口が固定の場合だけ使います。家庭回線の現在の公開 IP を固定すると、変更後に StunDeck が同期できなくなることがあります。運用周期に合わせて有効期限も設定できます。",[16,2634,2635],{},"「概要を表示する」を押し、対象 Zone に次の権限だけがあることを確認します。",[16,2637,2638],{},[1994,2639],{"alt":2640,"src":2641},"Cloudflare Token 概要の対象 Zone と最小権限","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-token-summary-v2.webp",[16,2643,2644],{},"最後に「Token を作成」を押します。平文は一度しか表示されません。パスワードマネージャーへ保存して StunDeck に貼り付け、Compose、Markdown、Git、ログ、スクリーンショット、チャットへ残さないでください。不要な長期 Credential を作らないため、この記事の画像は確認画面までで、Token 値は作成・表示していません。",[81,2646,2648],{"id":2647},"_7-stundeck-に-cloudflare-接続を保存して検査する","7. StunDeck に Cloudflare 接続を保存して検査する",[16,2650,2651,2652,2655],{},"サービスを作る前に StunDeck の ",[20,2653,2654],{},"Cloudflare"," ページを開きます。",[408,2657,2658,2661,2664,2667],{},[100,2659,2660],{},"識別しやすい接続名を入力；",[100,2662,2663],{},"新しい API Token を貼り付け；",[100,2665,2666],{},"「権限を検査」を実行；",[100,2668,2669],{},"Zone、DNS、Single Redirect がすべて成功してから保存。",[16,2671,2672],{},[1994,2673],{"alt":2674,"src":2675},"Token 欄が空の StunDeck Cloudflare 接続画面","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-connect-v2.webp",[16,2677,2678],{},[2001,2679,2680],{},"再構築後の実画面です。Token、アカウント情報、DNS 値、Resource ID は含まれません。",[16,2682,2683,2684,2686],{},"Credential はローカル master key で暗号化されます。復元可能なバックアップには SQLite と ",[25,2685,1457],{}," の両方が必要です。",[81,2688,2690],{"id":2689},"_8-302-を中心とした-redirect-サービスを作る","8. 302 を中心とした Redirect サービスを作る",[16,2692,2693],{},"停止可能な HTTP テストサービスを次のように設定します。",[33,2695,2698],{"className":2696,"code":2697,"language":233,"meta":38},[231],"名前：Home NAS（例）\nLAN ターゲット：192.168.1.20（例）\nターゲットポート：80（例）\nプロトコル：TCP\n待受ポート：0（自動）\nルーター開放：UPnP（推奨）\n公開方式：Cloudflare Redirect\nCloudflare 接続：前の手順で保存した接続\n入口ドメイン：stun-lab-20260802.sparkles-editor.com\nターゲット方式：HTTP\nRedirect status：302 Temporary\nDNS 自動管理：有効\nPath を保持：有効\nQuery を保持：有効\n",[25,2699,2697],{"__ignoreMap":38},[16,2701,2702],{},[1994,2703],{"alt":2704,"src":2705},"StunDeck サービスフォームの Cloudflare Redirect、入口ドメイン、302 設定","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-cf-redirect-service-v2.webp",[16,2707,2708],{},[2001,2709,2710],{},"実画面の文書用フォームです。LAN アドレスとポートはプレースホルダーで、Cloudflare 接続は保存せず、フォームも送信していません。上部の実サービスは裁切しています。",[248,2712,2714],{"id":2713},"_302-と-307-の選び方","302 と 307 の選び方",[141,2716,2717,2730],{},[144,2718,2719],{},[147,2720,2721,2724,2727],{},[150,2722,2723],{},"Status",[150,2725,2726],{},"Client の動作",[150,2728,2729],{},"推奨用途",[160,2731,2732,2743],{},[147,2733,2734,2737,2740],{},[165,2735,2736],{},"302 Temporary",[165,2738,2739],{},"一時 Redirect。一部 Client は POST を GET へ変更",[165,2741,2742],{},"Web ページ、Download、GET 中心のサービス。本稿の既定値",[147,2744,2745,2748,2751],{},[165,2746,2747],{},"307 Temporary Redirect",[165,2749,2750],{},"HTTP method と request body を保持",[165,2752,2753],{},"API、Webhook、Upload、POST の意味を維持するサービス",[16,2755,2756],{},"どちらも一時 Redirect です。変動する STUN 先に 301\u002F308 は使いません。",[16,2758,2759,2760,2763],{},"Path と Query の保持を有効にすると ",[25,2761,2762],{},"\u002Ffiles?a=1"," が現在のターゲットへ引き継がれます。HTTPS ターゲットには証明書がカバーする origin hostname も必要です。公開 IP へ直接 HTTPS Redirect すると通常は証明書検証に失敗します。",[16,2765,2766],{},"DNS 自動管理は StunDeck 自身が作成し、管理マークを付けたレコードだけを更新します。同名の非管理レコードがある場合、上書きせず同期を停止します。",[81,2768,2770],{"id":2769},"_9-サービスを開始して-stun-upnp-を診断する","9. サービスを開始して STUN \u002F UPnP を診断する",[16,2772,2773],{},"正常な遷移は次のとおりです。",[33,2775,2778],{"className":2776,"code":2777,"language":233,"meta":38},[231],"停止 → 探索中 → ルーター開放済み → Cloudflare 同期済み\n",[25,2779,2777],{"__ignoreMap":38},[16,2781,2782,2783,2786],{},"StunDeck はトラバーサル待受を",[20,2784,2785],{},"同じローカル待受ポート","へマッピングし、NATMap が検出した公開 IP とポートを Single Redirect のターゲットへ書き込みます。",[97,2788,2789,2792,2795,2798,2801,2804,2807,2810],{},[100,2790,2791],{},"Docker daemon とコンテナの proxy が空；",[100,2793,2794],{},"NATMap が実行可能；",[100,2796,2797],{},"LAN ターゲットへ到達可能；",[100,2799,2800],{},"選択したプロトコルの Binding が成功；",[100,2802,2803],{},"正しい UPnP IGD または NAT-PMP gateway を検出；",[100,2805,2806],{},"ルーター mapping が作成済み；",[100,2808,2809],{},"公開 mapping の更新時刻が新しい；",[100,2811,2812],{},"Cloudflare DNS と Redirect Rule が同期済み、または所有権競合を明示。",[16,2814,2815],{},[1994,2816],{"alt":2817,"src":2818},"proxy が空で Binding 成功を示す安全な StunDeck 診断画像","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-diagnostics-v1.webp",[16,2820,2821],{},[1994,2822],{"alt":2823,"src":2824},"1 件のサービス、1 件の検出済み mapping、実行 version を表示する StunDeck 概要","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fstundeck-overview-v1.webp",[81,2826,2828],{"id":2827},"_10-固定ドメインの-redirect-と第二段を両方検証する","10. 固定ドメインの Redirect と第二段を両方検証する",[16,2830,2831],{},"まず curl に Redirect を追従させず、Cloudflare の第一段だけを確認します。",[33,2833,2835],{"className":35,"code":2834,"language":37,"meta":38,"style":38},"curl -sS -o \u002Fdev\u002Fnull -D - --max-redirs 0 \\\n  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[25,2836,2837,2864],{"__ignoreMap":38},[42,2838,2839,2841,2844,2847,2850,2853,2856,2859,2862],{"class":44,"line":45},[42,2840,264],{"class":48},[42,2842,2843],{"class":52}," -sS",[42,2845,2846],{"class":52}," -o",[42,2848,2849],{"class":56}," \u002Fdev\u002Fnull",[42,2851,2852],{"class":52}," -D",[42,2854,2855],{"class":56}," -",[42,2857,2858],{"class":52}," --max-redirs",[42,2860,2861],{"class":52}," 0",[42,2863,273],{"class":52},[42,2865,2866],{"class":44,"line":276},[42,2867,2868],{"class":56},"  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[16,2870,2871,2874,2875,2877],{},[25,2872,2873],{},"HTTP\u002F2 302","（307 を選んだ場合は 307）と ",[25,2876,1350],{}," が必要です。Cloudflare Rules でも同じ入口ドメイン、status、active Rule を確認します。",[16,2879,2880],{},[1994,2881],{"alt":2882,"src":2883},"Cloudflare にある StunDeck 管理の 302 Single Redirect。動的ターゲットは裁切済み","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fcf-redirect-rule-v2.webp",[16,2885,2886],{},[2001,2887,2888],{},"実際の Rule 行です。公開許可済みの入口ドメインと 302 だけを残し、動的エンドポイント、アカウント情報、他 Rule、Resource ID は除外しました。",[16,2890,2891],{},"次にモバイル回線または独立した外部ホストから第二段を確認します。",[33,2893,2895],{"className":35,"code":2894,"language":37,"meta":38,"style":38},"curl -sS -L --max-time 15 -o \u002Fdev\u002Fnull \\\n  -w 'status=%{http_code} redirects=%{num_redirects}\\n' \\\n  https:\u002F\u002Fstun-lab-20260802.sparkles-editor.com\u002F\n",[25,2896,2897,2917,2927],{"__ignoreMap":38},[42,2898,2899,2901,2903,2905,2908,2911,2913,2915],{"class":44,"line":45},[42,2900,264],{"class":48},[42,2902,2843],{"class":52},[42,2904,53],{"class":52},[42,2906,2907],{"class":52}," --max-time",[42,2909,2910],{"class":52}," 15",[42,2912,2846],{"class":52},[42,2914,2849],{"class":56},[42,2916,273],{"class":52},[42,2918,2919,2922,2925],{"class":44,"line":276},[42,2920,2921],{"class":52},"  -w",[42,2923,2924],{"class":56}," 'status=%{http_code} redirects=%{num_redirects}\\n'",[42,2926,273],{"class":52},[42,2928,2929],{"class":44,"line":748},[42,2930,2868],{"class":56},[16,2932,2933,2934,2937,2938,2940],{},"目標は ",[25,2935,2936],{},"status=200 redirects=1"," です。再構築時には新しい動的 mapping へ外部ホストから接続して HTTP 200、固定入口から 302 と ",[25,2939,1350],{}," を確認しました。ただし Rule 画像の旧隔離サービスは後で停止したため、現在は第一段が残っている証拠であり、停止済み第二段の online 保証ではありません。",[16,2942,2943],{},[1994,2944],{"alt":2945,"src":2946},"LAN ターゲットと独立した外部第二段の HTTP 200 を匿名化した検証結果","\u002Fmedia\u002Fposts\u002Fstundeck-docker-deployment-guide\u002Fexternal-proof-verified-v1.webp",[16,2948,2949,2950,2952],{},"実際の ",[25,2951,1350],{},"、公開 IP、動的ポートは公開しないでください。",[81,2954,2956],{"id":2955},"_11-障害切り分け","11. 障害切り分け",[248,2958,2960,2961],{"id":2959},"公開マッピングがあるのに-err_empty_response","公開マッピングがあるのに ",[25,2962,2963],{},"ERR_EMPTY_RESPONSE",[408,2965,2966,2969,2975,2978,2981,2984,2987],{},[100,2967,2968],{},"StunDeck ホストから LAN ターゲットへ接続できるか；",[100,2970,2971,2972,2974],{},"アプリが ",[25,2973,218],{}," のみに bind していないか；",[100,2976,2977],{},"Compose が host network を使用しているか；",[100,2979,2980],{},"サービスの gateway mode が正しいか；",[100,2982,2983],{},"UPnP\u002FNAT-PMP が実際の出口 gateway に設定されたか；",[100,2985,2986],{},"上位の光回線終端装置や二段目ルーターが遮断していないか；",[100,2988,2989],{},"NAT loopback 非対応 LAN ではなく、別回線から試したか。",[248,2991,2993],{"id":2992},"stun-が不安定","STUN が不安定",[97,2995,2996,2999,3002,3005,3008,3011],{},[100,2997,2998],{},"Docker daemon とコンテナの proxy を両方確認；",[100,3000,3001],{},"透過 proxy、policy routing、multi-WAN を一時的に外して比較；",[100,3003,3004],{},"待受ポートを重複させない；",[100,3006,3007],{},"動的マッピングを固定値としてクライアントへ保存しない；",[100,3009,3010],{},"更新時刻と NATMap プロセスを監視；",[100,3012,3013],{},"NAT 境界が非対応なら Tunnel、VPN、中継へ切り替える。",[248,3015,3017],{"id":3016},"固定ドメインが-302307-を返さない","固定ドメインが 302\u002F307 を返さない",[408,3019,3020,3023,3026,3029,3032,3035],{},[100,3021,3022],{},"入口 DNS が proxy 状態か；",[100,3024,3025],{},"Token が正しい Zone に限定されているか；",[100,3027,3028],{},"Zone Read と Single Redirect Edit があるか；",[100,3030,3031],{},"DNS 自動管理時に DNS Edit があるか；",[100,3033,3034],{},"同名の非管理 DNS または Redirect Rule と競合していないか；",[100,3036,3037,3040],{},[25,3038,3039],{},"http_request_dynamic_redirect"," phase に active Rule があるか。",[248,3042,3044],{"id":3043},"cloudflare-入口は開くが第二段が失敗","Cloudflare 入口は開くが第二段が失敗",[16,3046,3047],{},"動的エンドポイントへ直接アクセスします。失敗するなら Redirect Rule ではなく、STUN、gateway mapping、firewall、LAN target を調べます。",[81,3049,3051],{"id":3050},"_12-更新バックアップ削除","12. 更新、バックアップ、削除",[16,3053,3054,3055,3057],{},"SQLite データベースと ",[25,3056,1457],{}," を必ず一緒にバックアップします。まず実際の volume 名を確認します。",[33,3059,3061],{"className":35,"code":3060,"language":37,"meta":38,"style":38},"sudo docker volume ls\nsudo docker volume inspect stundeck_stundeck-data\n",[25,3062,3063,3075],{"__ignoreMap":38},[42,3064,3065,3067,3069,3072],{"class":44,"line":45},[42,3066,1558],{"class":48},[42,3068,1929],{"class":56},[42,3070,3071],{"class":56}," volume",[42,3073,3074],{"class":56}," ls\n",[42,3076,3077,3079,3081,3083,3085],{"class":44,"line":276},[42,3078,1558],{"class":48},[42,3080,1929],{"class":56},[42,3082,3071],{"class":56},[42,3084,2418],{"class":56},[42,3086,3087],{"class":56}," stundeck_stundeck-data\n",[16,3089,3090],{},"更新：",[33,3092,3094],{"className":35,"code":3093,"language":37,"meta":38,"style":38},"cd \u002Fopt\u002Fstundeck\nsudo docker compose pull\nsudo docker compose up -d --no-build\nsudo docker compose ps\n",[25,3095,3096,3102,3112,3126],{"__ignoreMap":38},[42,3097,3098,3100],{"class":44,"line":45},[42,3099,2032],{"class":52},[42,3101,2027],{"class":56},[42,3103,3104,3106,3108,3110],{"class":44,"line":276},[42,3105,1558],{"class":48},[42,3107,1929],{"class":56},[42,3109,1941],{"class":56},[42,3111,2356],{"class":56},[42,3113,3114,3116,3118,3120,3122,3124],{"class":44,"line":748},[42,3115,1558],{"class":48},[42,3117,1929],{"class":56},[42,3119,1941],{"class":56},[42,3121,2367],{"class":56},[42,3123,1603],{"class":52},[42,3125,2372],{"class":52},[42,3127,3128,3130,3132,3134],{"class":44,"line":1590},[42,3129,1558],{"class":48},[42,3131,1929],{"class":56},[42,3133,1941],{"class":56},[42,3135,2383],{"class":56},[16,3137,3138],{},"データを保持して停止：",[33,3140,3142],{"className":35,"code":3141,"language":37,"meta":38,"style":38},"sudo docker compose down\n",[25,3143,3144],{"__ignoreMap":38},[42,3145,3146,3148,3150,3152],{"class":44,"line":45},[42,3147,1558],{"class":48},[42,3149,1929],{"class":56},[42,3151,1941],{"class":56},[42,3153,3154],{"class":56}," down\n",[16,3156,3157],{},"復元が不要な場合だけ volume を削除します。",[33,3159,3161],{"className":35,"code":3160,"language":37,"meta":38,"style":38},"sudo docker compose down -v\n",[25,3162,3163],{"__ignoreMap":38},[42,3164,3165,3167,3169,3171,3174],{"class":44,"line":45},[42,3166,1558],{"class":48},[42,3168,1929],{"class":56},[42,3170,1941],{"class":56},[42,3172,3173],{"class":56}," down",[42,3175,3176],{"class":52}," -v\n",[16,3178,3179,3182],{},[25,3180,3181],{},"down -v"," は管理者、サービス、イベント、暗号化された Cloudflare credential、ローカル master key を削除します。",[81,3184,3185],{"id":3185},"参考資料",[97,3187,3188,3195,3201,3208,3214,3221,3227,3234,3241,3248],{},[100,3189,3190],{},[387,3191,3194],{"href":3192,"rel":3193},"https:\u002F\u002Fgithub.com\u002FNciae-Zyh\u002Fstundeck",[391],"StunDeck リポジトリ",[100,3196,3197],{},[387,3198,3200],{"href":1401,"rel":3199},[391],"StunDeck v0.1.1 Release と FPK",[100,3202,3203],{},[387,3204,3207],{"href":3205,"rel":3206},"https:\u002F\u002Fdeveloper.fnnas.com\u002Fdocs\u002Fcli\u002Ffnpack",[391],"fnOS 開発者ドキュメント：fnpack",[100,3209,3210],{},[387,3211,3213],{"href":1541,"rel":3212},[391],"Docker：Install Docker Engine on Debian",[100,3215,3216],{},[387,3217,3220],{"href":3218,"rel":3219},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Finstall\u002Flinux\u002F",[391],"Docker：Install the Compose plugin",[100,3222,3223],{},[387,3224,3226],{"href":1525,"rel":3225},[391],"Lucky：STUN 内网穿透",[100,3228,3229],{},[387,3230,3233],{"href":3231,"rel":3232},"https:\u002F\u002Fgithub.com\u002Fheiher\u002Fnatmap",[391],"NATMap リポジトリ",[100,3235,3236],{},[387,3237,3240],{"href":3238,"rel":3239},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Fapi\u002Fget-started\u002Fcreate-token\u002F",[391],"Cloudflare：API Token を作成する",[100,3242,3243],{},[387,3244,3247],{"href":3245,"rel":3246},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-dashboard\u002F",[391],"Cloudflare：Dashboard で Single Redirect を作成する",[100,3249,3250],{},[387,3251,3254],{"href":3252,"rel":3253},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fsettings\u002F",[391],"Cloudflare：Single Redirect と proxied DNS の設定",[16,3256,3257,3258,3261],{},"重要なのは 5 層の証拠です。",[20,3259,3260],{},"ターゲット到達、STUN Binding 成功、gateway mapping 作成、Cloudflare 固定入口の 302\u002F307、独立した外部クライアントが Redirect を追従した後の成功","です。固定ドメインが解決するのは入口の安定性であり、第二段を保護された Tunnel に変えることではありません。",[744,3263,3264],{},"html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}",{"title":38,"searchDepth":748,"depth":748,"links":3266},[3267,3268,3269,3270,3273,3274,3275,3276,3277,3278,3281,3282,3283,3290,3291],{"id":1362,"depth":276,"text":1363},{"id":1468,"depth":276,"text":1468},{"id":1499,"depth":276,"text":1500},{"id":1534,"depth":276,"text":1535,"children":3271},[3272],{"id":1812,"depth":748,"text":1812},{"id":2006,"depth":276,"text":2007},{"id":2319,"depth":276,"text":2320},{"id":2467,"depth":276,"text":2468},{"id":2523,"depth":276,"text":2524},{"id":2647,"depth":276,"text":2648},{"id":2689,"depth":276,"text":2690,"children":3279},[3280],{"id":2713,"depth":748,"text":2714},{"id":2769,"depth":276,"text":2770},{"id":2827,"depth":276,"text":2828},{"id":2955,"depth":276,"text":2956,"children":3284},[3285,3287,3288,3289],{"id":2959,"depth":748,"text":3286},"公開マッピングがあるのに ERR_EMPTY_RESPONSE",{"id":2992,"depth":748,"text":2993},{"id":3016,"depth":748,"text":3017},{"id":3043,"depth":748,"text":3044},{"id":3050,"depth":276,"text":3051},{"id":3185,"depth":276,"text":3185},"fnOS に StunDeck FPK を手動導入する方法と Docker 導入を解説し、Cloudflare の最小権限 Token、プロキシ DNS、Single Redirect で固定ドメインを動的 STUN 公開先へ追従させます。",{"src":2824,"alt":3294,"caption":3295,"width":3296,"height":3297},"1 件のサービスと 1 件の公開マッピングを表示する StunDeck の概要画面","再構築した実環境の画面です。ユーザー名、パスワード、Token、プライベートアドレス、公開 IP、動的ポートは含みません。",1280,720,{},"\u002Fblog\u002Fstundeck-docker-deployment-guide","2026-08-03",35,{"title":1299,"description":3292},"blog\u002Fstundeck-docker-deployment-guide",[3305,3306,1371,3307,3308,3309,2654,1312,3310],"StunDeck","fnOS","Docker","STUN","UPnP","Homelab","2026-08-26",{"src":3313,"poster":2824,"title":3314,"description":3315,"duration":3316,"chapters":1674,"width":3317,"height":3318},"\u002Fmedia\u002Fvideos\u002Fstundeck-docker-deployment-guide\u002Fstundeck-complete-guide-zh-cn-v1.mp4","StunDeck 完全ガイド：Docker、Cloudflare 302\u002F307、STUN 診断","中国語ナレーションと中国語字幕付きの約 6 分の操作解説です。導入、初期設定、最小権限 Token、Redirect サービス、UPnP\u002FNAT-PMP、診断、外部回線検証までを扱います。",375,1920,1080,"NLGt9do6YC6GN3hqecI0UTKkjFG5WuBcqplOBc8Bw2g",{"id":3321,"title":3322,"accent":3323,"body":3324,"description":4922,"draft":775,"experience":776,"extension":777,"featured":775,"image":776,"meta":4923,"navigation":778,"path":4924,"publishedAt":4925,"readingTime":2282,"seo":4926,"stem":4927,"tags":4928,"updatedAt":776,"video":776,"__hash__":4931,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide.md","Lucky STUN + Cloudflare 302 実践：動的な公開ポートを固定ホスト名から開く","violet",{"type":13,"value":3325,"toc":4892},[3326,3337,3347,3357,3369,3372,3378,3381,3442,3446,3450,3457,3460,3474,3478,3481,3487,3496,3502,3507,3510,3514,3517,3523,3528,3627,3630,3636,3642,3646,3649,3652,3669,3673,3676,3693,3696,3700,3710,3714,3791,3806,3809,3813,3854,3860,3863,3869,3875,3879,3882,3888,3891,3897,3910,3914,3917,3923,3926,3944,3948,3955,3961,3964,4117,4124,4130,4133,4156,4164,4167,4173,4176,4180,4183,4205,4208,4225,4228,4247,4256,4260,4283,4286,4290,4303,4307,4310,4316,4319,4325,4330,4336,4356,4360,4362,4368,4371,4377,4382,4450,4453,4464,4467,4471,4474,4480,4492,4502,4505,4541,4545,4548,4554,4560,4563,4567,4570,4576,4584,4587,4589,4725,4728,4731,4734,4760,4771,4774,4780,4783,4786,4789,4792,4794,4889],[16,3327,3328,3329,3336],{},"本稿の中心は通常の DDNS ではなく、任意の STUN ポートを Cloudflare のリバースプロキシへ通す方法でもありません。",[20,3330,3331,3332,3335],{},"Lucky が STUN で ",[25,3333,3334],{},"公開 IP:動的ポート"," を取得し、マッピングの変更時に Webhook で Cloudflare Single Redirect の転送先を更新する","構成です。",[16,3338,3339,3340,3343,3344,3346],{},"利用者が覚えるのは ",[25,3341,3342],{},"go.example.com"," だけです。ブラウザーは最初に Cloudflare へアクセスして一時リダイレクトを受け取り、その後は現在の STUN エンドポイントへ直接接続します。参照記事の 301 と同じ発想ですが、本稿では ",[20,3345,1312],{}," を使います。NAT マッピングは一時的な値であり、ブラウザーや検索エンジンに恒久的な移転先として記憶させるべきではありません。",[16,3348,3349,3350,437,3353,3356],{},"本稿は Lucky 2.27.2 と Cloudflare Dashboard の実画面を使い、双方の公式ドキュメントで仕様を照合しています。文章中の例には ",[25,3351,3352],{},"example.com",[25,3354,3355],{},"192.0.2.0\u002F24"," など文書用の値だけを使用します。実写画像には実際のドメイン、IP、ポート、インスタンス名、通信量、Token を残さず、元の管理データがあった領域は不可逆な強いぼかしで処理しました。",[1355,3358,3359],{},[16,3360,3361,3362,3364,3365,3368],{},"結論：ブラウザー用の一時入口には 301 ではなく ",[20,3363,1312],{}," を使います。Cloudflare Single Redirect が現在正式に対応するのは 301、302、307、308 で、",[20,3366,3367],{},"303 は利用できません","。アプリ、API、リモートデスクトップ、非 HTTP プロトコルには、307、Cloudflare Tunnel、VPN、またはプロトコル固有の転送方法を検討します。",[81,3370,3371],{"id":3371},"実際のリクエスト経路",[33,3373,3376],{"className":3374,"code":3375,"language":233,"meta":38},[231],"最初のリクエスト\n\nブラウザーが https:\u002F\u002Fgo.example.com\u002Fphotos?id=42 を開く\n  └─ Cloudflare Single Redirect が 302 を返す\n       Location: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n        └─ ブラウザーが STUN マッピングへ直接接続\n             └─ Lucky 内蔵転送（またはルーターのポート転送）\n                  └─ 内部 Web サービス\n\nマッピングが変わったとき\n\nLucky が 203.0.113.10:51234 を取得\n  └─ STUN Webhook が Cloudflare Rulesets API を PATCH\n       └─ 次回アクセスから新しい Location を返す\n",[25,3377,3375],{"__ignoreMap":38},[16,3379,3380],{},"ここでいう「ポートなしのドメインアクセス」は、利用者が最初に入力する URL にポートがないという意味だけです。転送後のアドレスバーには実際の IP または転送先ホスト名とポートが表示されます。Cloudflare が処理するのは最初のリクエストだけであり、アプリ通信は Cloudflare を通りません。したがって WAF、Cache、Access、オリジン隠蔽は2本目の接続を保護しません。",[141,3382,3383,3399],{},[144,3384,3385],{},[147,3386,3387,3390,3393,3396],{},[150,3388,3389],{},"方法",[150,3391,3392],{},"利用者の入口",[150,3394,3395],{},"2本目の接続",[150,3397,3398],{},"適する用途",[160,3400,3401,3415,3428],{},[147,3402,3403,3406,3409,3412],{},[165,3404,3405],{},"STUN 直接接続",[165,3407,3408],{},"IP\u002Fホスト名 + 動的ポート",[165,3410,3411],{},"利用者から家庭回線へ直接",[165,3413,3414],{},"ポートを把握できる TCP\u002FUDP クライアント",[147,3416,3417,3420,3423,3425],{},[165,3418,3419],{},"STUN + Cloudflare 302",[165,3421,3422],{},"固定 HTTPS ホスト名から動的アドレスを表示",[165,3424,3411],{},[165,3426,3427],{},"接続先の公開を許容できるブラウザー GET ページ",[147,3429,3430,3433,3436,3439],{},[165,3431,3432],{},"Cloudflare Tunnel",[165,3434,3435],{},"固定 HTTPS ホスト名",[165,3437,3438],{},"Cloudflare Tunnel 経由",[165,3440,3441],{},"Web、API、管理画面、Access\u002FWAF が必要なサービス",[81,3443,3445],{"id":3444},"経路1lucky-stun-を設定する","経路1：Lucky STUN を設定する",[248,3447,3449],{"id":3448},"_1-最初にネットワーク条件を確認する","1. 最初にネットワーク条件を確認する",[16,3451,3452,3456],{},[387,3453,3455],{"href":1525,"rel":3454},[391],"Lucky の STUN 公式ドキュメント","は対象環境を NAT1 と説明し、公開ポートが固定される保証はなく、変更間隔にも規則性がないと明記しています。NAT タイプの呼び方は製品によって異なるため、ルーターに表示された文字だけで判断せず、Lucky のルールログと外部回線からの実接続で確認します。",[16,3458,3459],{},"ルールを作る前に、次を確認します。",[408,3461,3462,3465,3468,3471],{},[100,3463,3464],{},"ONU\u002F光モデム側で接続している場合、Lucky の案内どおり DMZ をメインルーターへ向けます。ルーター側で接続している場合は不要です。",[100,3466,3467],{},"Lucky をメインルーターで動かす構成が最も単純です。LAN 内ホストで動かす場合は、メインルーター側でも UPnP、ポート転送、または慎重に限定した DMZ が必要です。",[100,3469,3470],{},"OS カーネルとファイアウォールが対象の TCP\u002FUDP 動作を許可しているか確認します。プロキシ、二重 NAT、マルチ WAN は結果を変えることがあります。",[100,3472,3473],{},"最初の公開テストに Lucky の管理画面を使わないでください。機密データがなく、すぐ停止できるテストサービスを使います。",[248,3475,3477],{"id":3476},"_2-stun-モジュールを有効化する","2. STUN モジュールを有効化する",[16,3479,3480],{},"次の画面を開きます。",[33,3482,3485],{"className":3483,"code":3484,"language":233,"meta":38},[231],"Lucky → イントラネット貫通 → STUN → 設定\n",[25,3486,3484],{"__ignoreMap":38},[16,3488,3489,3490,3495],{},"モジュールを有効化し、グローバル STUN サーバー一覧を確認します。画面には公開の",[387,3491,3494],{"href":3492,"rel":3493},"https:\u002F\u002Fgithub.com\u002Fpradt2\u002Falways-online-stun\u002Fblob\u002Fmaster\u002Fvalid_hosts_tcp.txt",[391],"動作確認済み STUN ホスト一覧","へのリンクがあります。数が多ければ安定するとは限りません。自分の回線から到達でき、実際にテストしたサーバーを残し、出所不明の一覧を本番環境へ一括投入しないようにします。",[16,3497,3498],{},[1994,3499],{"alt":3500,"src":3501},"モジュール、公開サーバー一覧、Webhook のスイッチを表示する Lucky 2.27.2 の STUN 設定画面","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-settings-v1.png",[16,3503,3504],{},[2001,3505,3506],{},"実画面のキャプチャです。公開 STUN サーバーのアドレスだけを含み、プライベートネットワーク値や認証情報はありません。",[16,3508,3509],{},"Webhook は公開マッピングの変化を外部システムへ通知できます。具体的な自動化が必要な場合だけ有効にし、URL、ヘッダー、本文に含まれる秘密情報がログやスクリーンショットへ入らないようにします。",[248,3511,3513],{"id":3512},"_3-最初のルールはシンプルモードで作る","3. 最初のルールはシンプルモードで作る",[16,3515,3516],{},"貫通ルール一覧で「ルールを追加」を選び、初回テストはシンプルモードを使います。",[16,3518,3519],{},[1994,3520],{"alt":3521,"src":3522},"フォーム外にあった実際の管理データを強くぼかした Lucky の貫通ルール追加フォーム","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-stun-rule-dialog-redacted-v1.png",[16,3524,3525],{},[2001,3526,3527],{},"フォームは空欄です。背景の実ルール名、IP、ポート、通信量は不可逆な強いぼかしで処理しました。",[141,3529,3530,3539],{},[144,3531,3532],{},[147,3533,3534,3536],{},[150,3535,152],{},[150,3537,3538],{},"推奨設定",[160,3540,3541,3549,3557,3573,3581,3589,3597,3605,3619],{},[147,3542,3543,3546],{},[165,3544,3545],{},"ルール名",[165,3547,3548],{},"機器型番、設置場所、業務名を推測できない内部用ラベル",[147,3550,3551,3554],{},[165,3552,3553],{},"貫通タイプ",[165,3555,3556],{},"サービスに合わせて IPv4-TCP または IPv4-UDP。1ルールは1プロトコル",[147,3558,3559,3562],{},[165,3560,3561],{},"ローカルチャネルポート",[165,3563,3564,3565,3568,3569,3572],{},"手動指定では未使用かつネットワーク全体で一意の値。Lucky は ",[25,3566,3567],{},"10240"," より大きい値を推奨。UPnP 利用時はドキュメントに従い ",[25,3570,3571],{},"0"," で自動選択も可能",[147,3574,3575,3578],{},[165,3576,3577],{},"ファイアウォール自動許可",[165,3579,3580],{},"Lucky が対応ルーター上で動作し、作成されるルールを理解している場合だけ有効化",[147,3582,3583,3586],{},[165,3584,3585],{},"UPnP \u002F NAT-PMP",[165,3587,3588],{},"Lucky が LAN 内ホストで、ルーター側の対応機能が有効な場合だけ使用",[147,3590,3591,3594],{},[165,3592,3593],{},"Lucky 内蔵転送を使用しない",[165,3595,3596],{},"初回テストでは無効。貫通成功を確認してから性能目的のルーター転送を検討",[147,3598,3599,3602],{},[165,3600,3601],{},"チャネル検証を無効化",[165,3603,3604],{},"デバッグ用なので無効のままにする",[147,3606,3607,3610],{},[165,3608,3609],{},"転送先アドレス",[165,3611,3612,511,3615,3618],{},[25,3613,3614],{},"http:\u002F\u002F",[25,3616,3617],{},"https:\u002F\u002F"," を付けず、内部 IP またはホスト名を入力",[147,3620,3621,3624],{},[165,3622,3623],{},"転送先ポート",[165,3625,3626],{},"内部サービスの待受ポート。Lucky 内蔵転送では必須",[16,3628,3629],{},"完全に架空の TCP 例です。",[33,3631,3634],{"className":3632,"code":3633,"language":233,"meta":38},[231],"貫通タイプ：IPv4-TCP\nローカルチャネルポート：12080\n転送先アドレス：192.0.2.10\n転送先ポート：8080\n",[25,3635,3633],{"__ignoreMap":38},[16,3637,3638,3641],{},[25,3639,3640],{},"192.0.2.10:8080"," は RFC 5737 の文書用アドレスであり、チュートリアル用の値です。実際の内部 IP とポートは、スクリーンショットやログを公開する前にセットで隠してください。この組み合わせだけでサービスを推測できる場合があります。",[248,3643,3645],{"id":3644},"_4-根拠なくカスタムモードを調整しない","4. 根拠なくカスタムモードを調整しない",[16,3647,3648],{},"Lucky 2.27.2 のカスタムモードには、IP\u002Fインターフェース固定、IP 許可・拒否、TCP 接続数、UDP セッション数、暗号化、STUN 待機時間、ハートビート間隔、再試行、ログなどがあります。これらは相互に関係し、ある環境の初期値を別の環境へコピーする根拠にはなりません。",[16,3650,3651],{},"再現可能な理由がある場合だけ変更します。",[97,3653,3654,3657,3660,3663,3666],{},[100,3655,3656],{},"マルチ WAN やポリシールーティングでのみ、IP やインターフェースを固定する。",[100,3658,3659],{},"接続数と UDP セッション数は、サービスの同時利用数と機器資源に合わせて上限を設ける。",[100,3661,3662],{},"待機、ハートビート、再試行は一度に1項目だけ変え、変更前後のログを残す。",[100,3664,3665],{},"自動再試行は可用性を高める一方、誤設定による通信を継続させることもある。",[100,3667,3668],{},"TLS やストリーム暗号化は、アプリ側の認証・認可の代わりにはならない。",[248,3670,3672],{"id":3671},"_5-実際の外部回線でテストする","5. 実際の外部回線でテストする",[16,3674,3675],{},"保存後はルール固有のログを確認します。続いて Wi-Fi を切り、モバイル回線または別の固定回線から接続し、少なくとも次を確認します。",[408,3677,3678,3681,3684,3687,3690],{},[100,3679,3680],{},"Lucky が公開マッピングを取得したか。",[100,3682,3683],{},"外部接続がルールログに現れるか。",[100,3685,3686],{},"ファイアウォールまたはルーターが正しい Lucky ポートへ転送したか。",[100,3688,3689],{},"サービスが期待したアドレスとネットワーク名前空間で待ち受けているか。",[100,3691,3692],{},"公開ポート変更時に、クライアントや自動化へ新しい値を伝えられるか。",[16,3694,3695],{},"ログに外部からの試行が一切ない場合は、転送先サービスを何度も変える前に、ISP の NAT、モデム\u002Fルーターのマッピング、ファイアウォールを調べます。",[81,3697,3699],{"id":3698},"中心となる構成cloudflare-302-で-stun-の動的ポートを追従する","中心となる構成：Cloudflare 302 で STUN の動的ポートを追従する",[16,3701,3702,3703,3706,3707,3709],{},"STUN が返すのは公開 IP と公開ポートです。A\u002FAAAA レコードが保存できるのは IP だけで、通常のブラウザー URL にポートを動的に付加できません。参照構成の要点は、DNS が固定入口を Cloudflare まで届け、現在の ",[25,3704,3705],{},"IP:ポート"," をリダイレクトレスポンスの ",[25,3708,1350],{}," に保存し、Lucky Webhook がそれを更新し続けることです。",[248,3711,3713],{"id":3712},"_1-301-や-303-ではなく-302-を選ぶ理由","1. 301 や 303 ではなく 302 を選ぶ理由",[141,3715,3716,3729],{},[144,3717,3718],{},[147,3719,3720,3723,3726],{},[150,3721,3722],{},"ステータス",[150,3724,3725],{},"意味とメソッドの動作",[150,3727,3728],{},"本稿への適合性",[160,3730,3731,3745,3758,3769,3780],{},[147,3732,3733,3736,3739],{},[165,3734,3735],{},"301",[165,3737,3738],{},"恒久的な移転。クライアントや検索エンジンが転送先を長期保存する可能性がある",[165,3740,3741,3742],{},"動的 NAT には",[20,3743,3744],{},"不適切",[147,3746,3747,3749,3752],{},[165,3748,1312],{},[165,3750,3751],{},"一時的な移転。一般的なブラウザー GET ページに適する",[165,3753,3754,3755],{},"入口として",[20,3756,3757],{},"推奨",[147,3759,3760,3763,3766],{},[165,3761,3762],{},"303",[165,3764,3765],{},"次のリクエストを GET にする。POST 後の別リソース表示などで使う",[165,3767,3768],{},"Single Redirect では選択不可",[147,3770,3771,3774,3777],{},[165,3772,3773],{},"307",[165,3775,3776],{},"メソッドと本文を維持する一時転送",[165,3778,3779],{},"対応アプリ\u002FAPI で検討",[147,3781,3782,3785,3788],{},[165,3783,3784],{},"308",[165,3786,3787],{},"メソッドを維持する恒久転送",[165,3789,3790],{},"動的 NAT には不適切",[16,3792,3793,3794,3798,3799,3801,3802,3805],{},"Cloudflare の ",[387,3795,3797],{"href":3252,"rel":3796},[391],"Single Redirect 設定","が現在列挙するのは 301、302、307、308 です。Cloudflare の一般的な 3xx 文書が 303 の HTTP 意味を説明していても、Rulesets API の ",[25,3800,3039],{}," が 303 を受け付けることにはなりません。以下の JSON を ",[25,3803,3804],{},"status_code: 303"," に変更すると、API 検証で拒否される可能性があります。",[16,3807,3808],{},"303 が必須なら Cloudflare Worker または自分のオリジンで生成します。それは Single Redirect とは別の設計です。通常のブラウザー GET ページには 302 を使い、POST\u002FPUT 本文を維持すべき API は、単純に 307 へ置き換える前に構成自体を再検討します。",[248,3810,3812],{"id":3811},"_2-入口と直接接続先を別ホスト名にする","2. 入口と直接接続先を別ホスト名にする",[141,3814,3815,3827],{},[144,3816,3817],{},[147,3818,3819,3822,3825],{},[150,3820,3821],{},"ホスト名",[150,3823,3824],{},"Cloudflare モード",[150,3826,2573],{},[160,3828,3829,3841],{},[147,3830,3831,3835,3838],{},[165,3832,3833],{},[25,3834,3342],{},[165,3836,3837],{},"Proxied（オレンジクラウド）",[165,3839,3840],{},"リダイレクトを発生させる固定入口",[147,3842,3843,3848,3851],{},[165,3844,3845],{},[25,3846,3847],{},"origin-direct.example.com",[165,3849,3850],{},"DNS only（任意）",[165,3852,3853],{},"ホスト名に対応した TLS が必要な場合に Lucky DDNS で更新する直接接続先",[16,3855,3856,3857,3859],{},"Cloudflare は Single Redirect の対象となる受信ホスト名がプロキシされていることを要求します。そのため ",[25,3858,3342],{}," はオレンジクラウドにします。ルールが一致すれば、レコードのオリジンへ接続する前に 302 を返します。一致しない通信はオリジンへ進む可能性があるため、ホスト名全体を確実に対象にし、漏れたリクエストが管理画面へ届かず安全に失敗するようにします。",[16,3861,3862],{},"転送先には2つの形があります。",[33,3864,3867],{"className":3865,"code":3866,"language":233,"meta":38},[231],"最も単純： http:\u002F\u002F公開IP:動的ポート\u002Fpath\nHTTPS 向け： https:\u002F\u002Forigin-direct.example.com:動的ポート\u002Fpath\n",[25,3868,3866],{"__ignoreMap":38},[16,3870,3871,3872,3874],{},"HTTPS の IP へ直接転送すると、証明書のホスト名検証に失敗しやすくなります。HTTPS では Lucky DDNS で DNS only の転送先を更新し、",[25,3873,3847],{}," を含む証明書を用意します。どちらの場合も2本目の接続では IP とポートが公開され、Cloudflare WAF と Access を経由しません。",[248,3876,3878],{"id":3877},"_3-cloudflare-に-302-dynamic-redirect-を作成する","3. Cloudflare に 302 Dynamic Redirect を作成する",[16,3880,3881],{},"次を開きます。",[33,3883,3886],{"className":3884,"code":3885,"language":233,"meta":38},[231],"Cloudflare Dashboard → 対象 Zone → Rules → Redirect Rules\n→ Create rule → Single Redirects\n",[25,3887,3885],{"__ignoreMap":38},[16,3889,3890],{},"まずプレースホルダールールを作ります。",[33,3892,3895],{"className":3893,"code":3894,"language":233,"meta":38},[231],"ルール名：Lucky STUN temporary redirect\n一致式：(http.host eq \"go.example.com\")\nタイプ：Dynamic\n転送先式：concat(\"http:\u002F\u002F192.0.2.10:12080\", http.request.uri.path)\nステータス：302\nクエリ文字列を保持：有効\n",[25,3896,3894],{"__ignoreMap":38},[16,3898,3899,3902,3903,3906,3907,3909],{},[25,3900,3901],{},"192.0.2.10"," は文書用アドレスです。このルールは Cloudflare に ruleset と rule ID を作らせるためのもので、実接続先ではありません。デプロイ後は ",[25,3904,3905],{},"curl -I"," で 302 を確認し、",[25,3908,1350],{}," にパスとクエリが保持されているか調べます。",[248,3911,3913],{"id":3912},"_4-最小権限の-api-token-を作成する","4. 最小権限の API Token を作成する",[16,3915,3916],{},"Lucky 専用 Token を作ります。",[33,3918,3921],{"className":3919,"code":3920,"language":233,"meta":38},[231],"Permissions：Zone → Single Redirect → Edit\nResources：Include → Specific zone → example.com\n",[25,3922,3920],{"__ignoreMap":38},[16,3924,3925],{},"Global API Key を使わず、DNS、Workers、アカウント管理などの不要な権限を加えません。この Token は公開入口の転送先を変更できるため、漏えいするとフィッシング転送に悪用されます。",[16,3927,3928,3929,3932,3933,3936,3937,3940,3941,3943],{},"Webhook には秘密ではない3つの識別子、",[25,3930,3931],{},"ZONE_ID","、動的リダイレクトの ",[25,3934,3935],{},"RULESET_ID","、対象の ",[25,3938,3939],{},"RULE_ID"," も必要です。Dashboard でルールを編集する際のリクエストから確認するか、API で Zone ruleset を一覧し、phase が ",[25,3942,3039],{}," の項目とそのルールを読み取ります。公開スクリーンショットではこれらもマスクする方が安全です。",[248,3945,3947],{"id":3946},"_5-lucky-のルールに-webhook-を設定する","5. Lucky のルールに Webhook を設定する",[16,3949,3950,3951,3954],{},"対象 STUN ルールを編集して Webhook を有効化します。Dashboard 内部の ",[25,3952,3953],{},"\u002Fapi\u002Fv4\u002F"," URL ではなく、公開 API を使います。",[33,3956,3959],{"className":3957,"code":3958,"language":233,"meta":38},[231],"URL：\nhttps:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F\u003CZONE_ID>\u002Frulesets\u002F\u003CRULESET_ID>\u002Frules\u002F\u003CRULE_ID>\n\nメソッド：PATCH\n\nヘッダー：\nAuthorization: Bearer \u003CCLOUDFLARE_API_TOKEN>\nContent-Type: application\u002Fjson\n",[25,3960,3958],{"__ignoreMap":38},[16,3962,3963],{},"302 のリクエスト本文：",[33,3965,3969],{"className":3966,"code":3967,"language":3968,"meta":38,"style":38},"language-json shiki shiki-themes github-dark github-dark","{\n  \"action\": \"redirect\",\n  \"description\": \"Lucky STUN temporary redirect\",\n  \"enabled\": true,\n  \"expression\": \"(http.host eq \\\"go.example.com\\\")\",\n  \"action_parameters\": {\n    \"from_value\": {\n      \"status_code\": 302,\n      \"target_url\": {\n        \"expression\": \"concat(\\\"http:\u002F\u002F#{ipAddr}\\\", http.request.uri.path)\"\n      },\n      \"preserve_query_string\": true\n    }\n  }\n}\n","json",[25,3970,3971,3976,3989,4001,4013,4035,4043,4050,4061,4068,4088,4093,4102,4107,4112],{"__ignoreMap":38},[42,3972,3973],{"class":44,"line":45},[42,3974,3975],{"class":1788},"{\n",[42,3977,3978,3981,3983,3986],{"class":44,"line":276},[42,3979,3980],{"class":52},"  \"action\"",[42,3982,2071],{"class":1788},[42,3984,3985],{"class":56},"\"redirect\"",[42,3987,3988],{"class":1788},",\n",[42,3990,3991,3994,3996,3999],{"class":44,"line":748},[42,3992,3993],{"class":52},"  \"description\"",[42,3995,2071],{"class":1788},[42,3997,3998],{"class":56},"\"Lucky STUN temporary redirect\"",[42,4000,3988],{"class":1788},[42,4002,4003,4006,4008,4011],{"class":44,"line":1590},[42,4004,4005],{"class":52},"  \"enabled\"",[42,4007,2071],{"class":1788},[42,4009,4010],{"class":52},"true",[42,4012,3988],{"class":1788},[42,4014,4015,4018,4020,4023,4026,4028,4030,4033],{"class":44,"line":1609},[42,4016,4017],{"class":52},"  \"expression\"",[42,4019,2071],{"class":1788},[42,4021,4022],{"class":56},"\"(http.host eq ",[42,4024,4025],{"class":52},"\\\"",[42,4027,3342],{"class":56},[42,4029,4025],{"class":52},[42,4031,4032],{"class":56},")\"",[42,4034,3988],{"class":1788},[42,4036,4037,4040],{"class":44,"line":1624},[42,4038,4039],{"class":52},"  \"action_parameters\"",[42,4041,4042],{"class":1788},": {\n",[42,4044,4045,4048],{"class":44,"line":1633},[42,4046,4047],{"class":52},"    \"from_value\"",[42,4049,4042],{"class":1788},[42,4051,4052,4055,4057,4059],{"class":44,"line":1646},[42,4053,4054],{"class":52},"      \"status_code\"",[42,4056,2071],{"class":1788},[42,4058,1312],{"class":52},[42,4060,3988],{"class":1788},[42,4062,4063,4066],{"class":44,"line":1651},[42,4064,4065],{"class":52},"      \"target_url\"",[42,4067,4042],{"class":1788},[42,4069,4070,4073,4075,4078,4080,4083,4085],{"class":44,"line":1674},[42,4071,4072],{"class":52},"        \"expression\"",[42,4074,2071],{"class":1788},[42,4076,4077],{"class":56},"\"concat(",[42,4079,4025],{"class":52},[42,4081,4082],{"class":56},"http:\u002F\u002F#{ipAddr}",[42,4084,4025],{"class":52},[42,4086,4087],{"class":56},", http.request.uri.path)\"\n",[42,4089,4090],{"class":44,"line":1680},[42,4091,4092],{"class":1788},"      },\n",[42,4094,4095,4098,4100],{"class":44,"line":1686},[42,4096,4097],{"class":52},"      \"preserve_query_string\"",[42,4099,2071],{"class":1788},[42,4101,2104],{"class":52},[42,4103,4104],{"class":44,"line":788},[42,4105,4106],{"class":1788},"    }\n",[42,4108,4109],{"class":44,"line":1697},[42,4110,4111],{"class":1788},"  }\n",[42,4113,4114],{"class":44,"line":1703},[42,4115,4116],{"class":1788},"}\n",[16,4118,4119,4120,4123],{},"STUN Webhook 実行時、Lucky は ",[25,4121,4122],{},"#{ipAddr}"," を現在の公開アドレスとポートへ置換します。Cloudflare には次のような式が保存されます。",[33,4125,4128],{"className":4126,"code":4127,"language":233,"meta":38},[231],"concat(\"http:\u002F\u002F203.0.113.10:45678\", http.request.uri.path)\n",[25,4129,4127],{"__ignoreMap":38},[16,4131,4132],{},"DNS only の転送先ホスト名と HTTPS を使う場合、Lucky DDNS でそのホスト名を更新し、式を次のようにします。",[33,4134,4136],{"className":3966,"code":4135,"language":3968,"meta":38,"style":38},"\"expression\": \"concat(\\\"https:\u002F\u002Forigin-direct.example.com:#{port}\\\", http.request.uri.path)\"\n",[25,4137,4138],{"__ignoreMap":38},[42,4139,4140,4143,4145,4147,4149,4152,4154],{"class":44,"line":45},[42,4141,4142],{"class":56},"\"expression\"",[42,4144,2071],{"class":1788},[42,4146,4077],{"class":56},[42,4148,4025],{"class":52},[42,4150,4151],{"class":56},"https:\u002F\u002Forigin-direct.example.com:#{port}",[42,4153,4025],{"class":52},[42,4155,4087],{"class":56},[16,4157,4158,467,4160,4163],{},[25,4159,4122],{},[25,4161,4162],{},"#{port}"," は Lucky Webhook のテンプレート変数であり、Cloudflare Rules 言語の変数ではありません。半角の引用符を使い、JSON 内部の式の引用符にはバックスラッシュのエスケープを残します。",[16,4165,4166],{},"成功判定文字列：",[33,4168,4171],{"className":4169,"code":4170,"language":233,"meta":38},[231],"\"success\": true\n",[25,4172,4170],{"__ignoreMap":38},[16,4174,4175],{},"レスポンスの整形によって空白が入る場合があります。手動実行が1回成功しただけで長期運用可能と判断せず、Lucky の Webhook ログと Cloudflare API レスポンスを確認し、ルールを再取得して実際に転送先が変わったことを検証します。",[248,4177,4179],{"id":4178},"_6-別回線から経路全体を検証する","6. 別回線から経路全体を検証する",[16,4181,4182],{},"モバイル回線などから最初の応答を調べます。",[33,4184,4186],{"className":35,"code":4185,"language":37,"meta":38,"style":38},"curl -sS -D - -o \u002Fdev\u002Fnull \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n",[25,4187,4188],{"__ignoreMap":38},[42,4189,4190,4192,4194,4196,4198,4200,4202],{"class":44,"line":45},[42,4191,264],{"class":48},[42,4193,2843],{"class":52},[42,4195,2852],{"class":52},[42,4197,2855],{"class":56},[42,4199,2846],{"class":52},[42,4201,2849],{"class":56},[42,4203,4204],{"class":56}," \"https:\u002F\u002Fgo.example.com\u002Fphotos?id=42\"\n",[16,4206,4207],{},"期待する主要部分：",[33,4209,4213],{"className":4210,"code":4211,"language":4212,"meta":38,"style":38},"language-http shiki shiki-themes github-dark github-dark","HTTP\u002F2 302\nlocation: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n","http",[25,4214,4215,4220],{"__ignoreMap":38},[42,4216,4217],{"class":44,"line":45},[42,4218,4219],{},"HTTP\u002F2 302\n",[42,4221,4222],{"class":44,"line":276},[42,4223,4224],{},"location: http:\u002F\u002F203.0.113.10:45678\u002Fphotos?id=42\n",[16,4226,4227],{},"続けて次を確認します。",[408,4229,4230,4233,4238,4241,4244],{},[100,4231,4232],{},"古いルールの 301 ではなく 302 である。",[100,4234,4235,4237],{},[25,4236,1350],{}," の IP、ポート、パス、クエリが正しい。",[100,4239,4240],{},"STUN マッピング更新後、Webhook が新しいポートを公開する。",[100,4242,4243],{},"シークレットウィンドウと別端末が恒久リダイレクトのキャッシュを使っていない。",[100,4245,4246],{},"直接公開サービス自体に認証、レート制限、総当たり対策、TLS がある。",[16,4248,4249,4251,4252,4255],{},[25,4250,3905],{}," は HEAD のみを検証し、サービスによっては GET と動作が異なります。上記の ",[25,4253,4254],{},"-D - -o \u002Fdev\u002Fnull"," は GET を送りつつ、非公開の本文を端末へ出力しません。",[248,4257,4259],{"id":4258},"_7-302-構成では解決しないこと","7. 302 構成では解決しないこと",[97,4261,4262,4268,4271,4274,4277,4280],{},[100,4263,4264,4265,4267],{},"ポートを本当に隠すものではなく、アドレスバーと ",[25,4266,1350],{}," に表示される。",[100,4269,4270],{},"家庭回線の公開 IP を隠さず、利用者はそこへ直接接続する。",[100,4272,4273],{},"アプリ、テレビクライアント、リモートデスクトップ、独自プロトコルへ HTTP リダイレクト対応を追加しない。",[100,4275,4276],{},"2本目のリクエストを Cloudflare WAF、Cache、Access の背後へ置かない。",[100,4278,4279],{},"Cookie Domain、CORS、CSRF、絶対 URL、Host 検証、TLS 証明書を自動解決しない。",[100,4281,4282],{},"HTTP\u002FHTTPS の入口用であり、UDP セッションを HTTP 302 で確立することはできない。",[16,4284,4285],{},"Cloudflare Redirect は STUN ポート用のオレンジクラウドプロキシではありません。オレンジクラウドが処理するのは固定入口への最初の HTTP リクエストだけです。",[81,4287,4289],{"id":4288},"代替案lucky-で-cloudflare-tunnel-を動かす","代替案：Lucky で Cloudflare Tunnel を動かす",[16,4291,4292,4293,4296,4297,4302],{},"Web、API、管理画面では Cloudflare Tunnel の方が単純なことが多いです。",[25,4294,4295],{},"cloudflared"," が内部から外向きに接続するため、公開 IP もルーターのインバウンドポートも不要です。仕組みは ",[387,4298,4301],{"href":4299,"rel":4300},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002F",[391],"Cloudflare Tunnel 公式概要","で確認できます。",[248,4304,4306],{"id":4305},"_1-cloudflare-でリモート管理-tunnel-を作る","1. Cloudflare でリモート管理 Tunnel を作る",[16,4308,4309],{},"Cloudflare Dashboard で次を開きます。",[33,4311,4314],{"className":4312,"code":4313,"language":233,"meta":38},[231],"Networking → Tunnels → Create a tunnel → Cloudflared\n",[25,4315,4313],{"__ignoreMap":38},[16,4317,4318],{},"自宅住所、機器シリアル、個人名を含まない名前を付けます。作成後、Dashboard は Tunnel Token を含むインストールコマンドを表示します。Lucky に必要なのは Token の値だけです。コマンド全体のスクリーンショットや長期保存は避けます。",[16,4320,4321],{},[1994,4322],{"alt":4323,"src":4324},"Cloudflare Dashboard の空の Tunnel 作成フォーム","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Fcloudflare-create-tunnel-v1.png",[16,4326,4327],{},[2001,4328,4329],{},"実画面のキャプチャです。名前は未入力で、Token は生成されず、Tunnel の作成も実行していません。アカウントのサイドバーは折りたたまれています。",[16,4331,4332,4333,4335],{},"Tunnel Token は新しい ",[25,4334,4295],{}," コネクターをその Tunnel へ参加させられるため、パスワードとして扱います。",[97,4337,4338,4341,4344,4347],{},[100,4339,4340],{},"Markdown、シェル履歴、Git、Dockerfile、公開環境変数へ書かない。",[100,4342,4343],{},"ブラウザー側コードへ渡さない。",[100,4345,4346],{},"Tunnel ごとに別の Token を使う。",[100,4348,4349,4350,4355],{},"漏えい時は Tunnel 詳細で Token を更新し、古い接続を削除する。Cloudflare も",[387,4351,4354],{"href":4352,"rel":4353},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fconfigure-tunnels\u002Fremote-tunnel-permissions\u002F",[391],"定期的な Tunnel Token のローテーション","を推奨しています。",[248,4357,4359],{"id":4358},"_2-lucky-に-tunnel-インスタンスを追加する","2. Lucky に Tunnel インスタンスを追加する",[16,4361,3881],{},[33,4363,4366],{"className":4364,"code":4365,"language":233,"meta":38},[231],"Lucky → イントラネット貫通 → Cloudflared → インスタンスを追加\n",[25,4367,4365],{"__ignoreMap":38},[16,4369,4370],{},"「Tunnel」モードを選び、次を設定します。",[16,4372,4373],{},[1994,4374],{"alt":4375,"src":4376},"フォーム外の実際のインスタンス情報を強くぼかした Lucky の Cloudflared インスタンス追加フォーム","\u002Fmedia\u002Fposts\u002Flucky-stun-cloudflare-tunnel-guide\u002Flucky-cloudflared-instance-redacted-v1.png",[16,4378,4379],{},[2001,4380,4381],{},"Tunnel Token と API Token は空欄です。背景の既存インスタンス情報は不可逆な強いぼかしで処理しました。",[141,4383,4384,4392],{},[144,4385,4386],{},[147,4387,4388,4390],{},[150,4389,152],{},[150,4391,3538],{},[160,4393,4394,4402,4410,4418,4426,4434,4442],{},[147,4395,4396,4399],{},[165,4397,4398],{},"インスタンス備考",[165,4400,4401],{},"機器や場所を漏らさない一意のラベル",[147,4403,4404,4407],{},[165,4405,4406],{},"Token",[165,4408,4409],{},"Tunnel Token を貼り付ける。公開前は値全体を隠す",[147,4411,4412,4415],{},[165,4413,4414],{},"エッジネットワーク",[165,4416,4417],{},"通常は自動。IPv4\u002FIPv6 の問題を診断できた場合だけ固定",[147,4419,4420,4423],{},[165,4421,4422],{},"高可用性接続数",[165,4424,4425],{},"適切な数を維持する。接続数を増やしても無制限の可用性にはならない",[147,4427,4428,4431],{},[165,4429,4430],{},"接続プロトコル",[165,4432,4433],{},"Auto、HTTP\u002F2、QUIC は回線次第。アウトバウンド UDP が制限される場合は HTTP\u002F2 と比較",[147,4435,4436,4439],{},[165,4437,4438],{},"エッジ bind IP",[165,4440,4441],{},"単一回線では空欄。マルチ WAN で出口を指定するときだけ利用",[147,4443,4444,4447],{},[165,4445,4446],{},"転送先 TLS 証明書を検証しない",[165,4448,4449],{},"本番では無効化し、正しい Origin Server Name または信頼 CA を使う",[16,4451,4452],{},"Lucky からアプリケーションルートと DNS を自動管理する場合は、Tunnel Token とは別の Cloudflare API Token を設定します。現在の Lucky 画面では次の権限が案内されています。",[97,4454,4455,4458,4461],{},[100,4456,4457],{},"Account → Cloudflare Tunnel → Edit",[100,4459,4460],{},"Zone → DNS → Edit",[100,4462,4463],{},"Account と Zone の範囲を実際に使う対象へ限定",[16,4465,4466],{},"自動管理が不要なら API Token を空欄にし、Cloudflare Dashboard 側でルートと DNS を管理できます。「All」を選ぶ便利さより、最小権限を優先します。",[248,4468,4470],{"id":4469},"_3-アプリケーションルートを追加する","3. アプリケーションルートを追加する",[16,4472,4473],{},"Tunnel インスタンスの「Application routes」で、次のようなマッピングを追加します。",[33,4475,4478],{"className":4476,"code":4477,"language":233,"meta":38},[231],"ホスト名：app.example.com\nパス：\u002F*\n転送先サービス：http:\u002F\u002F192.0.2.10:8080\n",[25,4479,4477],{"__ignoreMap":38},[16,4481,4482,4483,4488,4489,4491],{},"転送先サービスにはプロトコルが必要です。HTTP、HTTPS、TCP などの指定方法は ",[387,4484,4487],{"href":4485,"rel":4486},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Frouting\u002F",[391],"Cloudflare Tunnel のルーティング文書","を参照してください。一般的なブラウザー公開では HTTP\u002FHTTPS の Published application route を優先します。非 HTTP の公開ホスト名はクライアント側にも ",[25,4490,4295],{}," が必要になる場合があり、任意 TCP\u002FUDP の公開プロキシには Spectrum またはプライベートネットワーク経路が必要な場合があります。",[16,4493,4494,4495,23,4498,4501],{},"同一ホスト名に複数のパスルールがある場合、具体的なルールを catch-all より前に置きます。たとえば ",[25,4496,4497],{},"\u002Fapi\u002F*",[25,4499,4500],{},"\u002F*"," より前です。広いルールが先に一致すると、後続ルールへ到達しません。",[16,4503,4504],{},"詳細設定は必要な場合だけ利用します。",[97,4506,4507,4513,4519,4525,4531],{},[100,4508,4509,4512],{},[20,4510,4511],{},"Origin Server Name","：証明書のホスト名と転送先アドレスが異なる場合。",[100,4514,4515,4518],{},[20,4516,4517],{},"Host ヘッダー","：転送先が仮想ホストで振り分ける場合。",[100,4520,4521,4524],{},[20,4522,4523],{},"HTTP\u002F2 to origin","：転送先が対応している場合だけ。",[100,4526,4527,4530],{},[20,4528,4529],{},"接続、TLS、keep-alive timeout","：ログとサービス特性に基づいて調整。",[100,4532,4533,4536,4537,4540],{},[20,4534,4535],{},"TLS 検証を無効化","：一時的な切り分けに限定。Cloudflare も ",[25,4538,4539],{},"noTLSVerify"," を本番非推奨の最終手段としています。",[248,4542,4544],{"id":4543},"_4-cname-を設定する","4. CNAME を設定する",[16,4546,4547],{},"各 Tunnel には次の形式の転送先が割り当てられます。",[33,4549,4552],{"className":4550,"code":4551,"language":233,"meta":38},[231],"\u003CTUNNEL_ID>.cfargotunnel.com\n",[25,4553,4551],{"__ignoreMap":38},[16,4555,4556,4559],{},[25,4557,4558],{},"app.example.com"," の CNAME をこの値へ向けます。Cloudflare Dashboard で Published application route を追加すると、通常は DNS レコードも自動作成されます。Lucky からルートを管理する場合、自動作成されるかは API Token と設定に依存します。保存後は DNS 画面で実際のレコードを確認します。",[16,4561,4562],{},"Tunnel UUID は Token と同じ認証秘密情報ではありませんが、インフラ識別子です。記事で実値を見せる利点はないため、同様に伏せます。",[248,4564,4566],{"id":4565},"_5-管理画面を-cloudflare-access-で保護する","5. 管理画面を Cloudflare Access で保護する",[16,4568,4569],{},"Tunnel は転送先への経路を作りますが、「誰が利用できるか」を自動では決めません。管理画面、監視、個人向けサービスには Access アプリケーションを追加します。",[33,4571,4574],{"className":4572,"code":4573,"language":233,"meta":38},[231],"Zero Trust → Access controls → Applications\n→ Create new application → Self-hosted and private\n",[25,4575,4573],{"__ignoreMap":38},[16,4577,4578,4579,4302],{},"Tunnel の公開ホスト名を登録し、メールアドレス、ID プロバイダー、端末状態、Service Token などを使った Allow ポリシーを作ります。Action、Include、Require、Exclude の構造は ",[387,4580,4583],{"href":4581,"rel":4582},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Faccess-controls\u002Fpolicies\u002F",[391],"Access ポリシー公式文書",[16,4585,4586],{},"Access で保護する公開 URL はポート番号に依存させないでください。Cloudflare のポート文書によると、Access は URL のポート番号を除去します。外部には標準 HTTPS ホスト名を公開し、内部ポートは Tunnel の転送先だけに保持します。",[81,4588,618],{"id":618},[141,4590,4591,4601],{},[144,4592,4593],{},[147,4594,4595,4598],{},[150,4596,4597],{},"症状",[150,4599,4600],{},"最初に確認する項目",[160,4602,4603,4611,4619,4627,4635,4647,4658,4666,4674,4682,4690,4698,4706,4717],{},[147,4604,4605,4608],{},[165,4606,4607],{},"STUN がマッピングを取得しない",[165,4609,4610],{},"NAT 条件、接続を行う機器、二重 NAT、プロキシ、カーネル、ファイアウォール",[147,4612,4613,4616],{},[165,4614,4615],{},"STUN のマッピングはあるが外部試行がログにない",[165,4617,4618],{},"公開ポート、ファイアウォール、ルーター転送、ISP の入通信制限",[147,4620,4621,4624],{},[165,4622,4623],{},"外部試行はあるがサービスへ届かない",[165,4625,4626],{},"転送先アドレス、ポート、プロトコル、待受アドレス",[147,4628,4629,4632],{},[165,4630,4631],{},"DNS は解決するがポートが違う",[165,4633,4634],{},"A\u002FAAAA は IP のみ。ポート変更通知とクライアント設定を確認",[147,4636,4637,4640],{},[165,4638,4639],{},"入口がまだ 301 を返す",[165,4641,4642,4643,4646],{},"より前に一致する Redirect\u002FPage Rule がないか、現在の ",[25,4644,4645],{},"status_code"," を確認",[147,4648,4649,4652],{},[165,4650,4651],{},"Webhook が API エラーになる",[165,4653,4654,4657],{},[25,4655,4656],{},"Single Redirect: Edit","、Zone\u002Fルール ID、JSON の引用符と式のエスケープを確認",[147,4659,4660,4663],{},[165,4661,4662],{},"302 の Location が古いポートのまま",[165,4664,4665],{},"Lucky が Webhook を実行したか、成功判定が誤検知していないか、別の自動化がルールを上書きしていないか確認",[147,4667,4668,4671],{},[165,4669,4670],{},"ブラウザーは動くがアプリでログインできない",[165,4672,4673],{},"リダイレクト対応、Host、Cookie、認証コールバック、TLS のホスト名前提を確認",[147,4675,4676,4679],{},[165,4677,4678],{},"HTTPS の IP へ転送すると証明書エラー",[165,4680,4681],{},"証明書と一致する DNS only の転送先ホスト名を使うか Tunnel へ変更。証明書検証は無効化しない",[147,4683,4684,4687],{},[165,4685,4686],{},"Tunnel は接続済みだが 502",[165,4688,4689],{},"Lucky から転送先への到達性、プロトコル、ポート、待受アドレス",[147,4691,4692,4695],{},[165,4693,4694],{},"HTTPS 転送先で x509 エラー",[165,4696,4697],{},"正しい Origin Server Name または CA を設定し、恒久的に検証を飛ばさない",[147,4699,4700,4703],{},[165,4701,4702],{},"Tunnel ホスト名で 1016",[165,4704,4705],{},"CNAME のアカウント、Tunnel の稼働、UUID",[147,4707,4708,4711],{},[165,4709,4710],{},"パスが別サービスへ届く",[165,4712,4713,4714,4716],{},"ルート順序。具体的パスを ",[25,4715,4500],{}," より前にする",[147,4718,4719,4722],{},[165,4720,4721],{},"QUIC が接続できない",[165,4723,4724],{},"アウトバウンド UDP を確認し、HTTP\u002F2 と比較",[16,4726,4727],{},"一度に変更する変数は1つにし、変更時刻を記録します。ログを共有する前に、公開\u002F内部 IP、ポート、ホスト名、リクエストパス、Cookie、Authorization、Token、インスタンス名を削除します。",[81,4729,4730],{"id":4730},"スクリーンショット公開前のプライバシーチェック",[16,4732,4733],{},"Lucky のルール一覧は情報量が多く、1枚で家庭内ネットワーク構成が見えることがあります。画像ごとに次を確認します。",[97,4735,4736,4739,4742,4745,4748,4751,4754,4757],{},[100,4737,4738],{},"Lucky 管理画面のアドレス、ポート、非公開入口パス。",[100,4740,4741],{},"公開 IP、マッピングポート、内部 IP、転送先ポート。",[100,4743,4744],{},"ドメイン、ホスト名、インスタンス名、ルール名、機器名。",[100,4746,4747],{},"Tunnel Token、Cloudflare API Token、Access Service Token。",[100,4749,4750],{},"Account ID、Zone ID、Tunnel UUID、CNAME 転送先。",[100,4752,4753],{},"リクエストパス、Webhook URL、リクエストヘッダー。",[100,4755,4756],{},"ログ、訪問者 IP、通信量、オン\u002Fオフ状態。",[100,4758,4759],{},"ブラウザーのタブ、ブックマーク、プロフィール画像、メールアドレス。",[16,4761,4762,4763,437,4765,437,4767,4770],{},"Token の中央だけをぼかしても安全とは限りません。実値をすべて ",[25,4764,4558],{},[25,4766,3901],{},[25,4768,4769],{},"\u003CTOKEN>"," に置き換えた再作図の方が安全です。",[81,4772,4773],{"id":4773},"まとめ",[16,4775,4776,4777,4779],{},"少人数の信頼できる利用者向けブラウザー GET ページで、転送後の IP とポート公開を許容でき、NAT 条件を確認済みなら ",[20,4778,3419],{}," を選べます。解決するのは「固定入口を1つ覚える」ことであり、リバースプロキシ、セキュリティゲートウェイ、本当のポート隠蔽ではありません。",[16,4781,4782],{},"メソッド維持が必要なら、クライアントを検証してから 307 を検討します。Single Redirect の JSON に 303 を入れないでください。303 が必須なら Worker またはオリジンで生成し、動的転送先の保存と認証を別途設計します。",[16,4784,4785],{},"標準 HTTPS ホスト名を維持し、Access や WAF を使いたい Web\u002FAPI には Cloudflare Tunnel を選びます。STUN の成功に依存せず、NAT マッピングが変わっても公開 URL は変わりません。",[16,4787,4788],{},"直接 TCP\u002FUDP、短い通信経路が重要で、Lucky の STUN 条件を満たし、公開ポートの変化、DNS only による転送元の直接公開、認証とファイアウォールの自己管理を受け入れられる場合は STUN を選びます。",[16,4790,4791],{},"STUN 直接接続、STUN + 302 の入口、Cloudflare Tunnel はいずれも内部サービスを公開できますが、アプリ通信が Cloudflare の経路に残るのは最後の方式だけです。301 のリクエスト本文をコピーする前に、プロトコル、クライアント動作、セキュリティ境界を決めます。",[81,4793,3185],{"id":3185},[97,4795,4796,4802,4809,4816,4823,4829,4836,4843,4850,4856,4863,4869,4876,4882],{},[100,4797,4798],{},[387,4799,4801],{"href":1525,"rel":4800},[391],"Lucky：STUN による NAT 越え",[100,4803,4804],{},[387,4805,4808],{"href":4806,"rel":4807},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fcloudflared",[391],"Lucky：Cloudflare Tunnel",[100,4810,4811],{},[387,4812,4815],{"href":4813,"rel":4814},"https:\u002F\u002Flucky666.cn\u002Fdocs\u002Fmodules\u002Fddns",[391],"Lucky：Dynamic DNS",[100,4817,4818],{},[387,4819,4822],{"href":4820,"rel":4821},"https:\u002F\u002Fwww.geekxw.top\u002F1814\u002F",[391],"参照した方式：Lucky STUN + Cloudflare 301 リダイレクト",[100,4824,4825],{},[387,4826,4828],{"href":3252,"rel":4827},[391],"Cloudflare Single Redirect のステータスと動的式",[100,4830,4831],{},[387,4832,4835],{"href":4833,"rel":4834},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Frules\u002Furl-forwarding\u002Fsingle-redirects\u002Fcreate-api\u002F",[391],"Cloudflare API で Single Redirect を作成する",[100,4837,4838],{},[387,4839,4842],{"href":4840,"rel":4841},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fapi\u002Fresources\u002Frulesets\u002F",[391],"Cloudflare Rulesets API",[100,4844,4845],{},[387,4846,4849],{"href":4847,"rel":4848},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fsupport\u002Ftroubleshooting\u002Fhttp-status-codes\u002F3xx-redirection\u002F",[391],"Cloudflare の 3xx ステータスコード",[100,4851,4852],{},[387,4853,4855],{"href":4299,"rel":4854},[391],"Cloudflare Tunnel 概要",[100,4857,4858],{},[387,4859,4862],{"href":4860,"rel":4861},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fget-started\u002Fcreate-remote-tunnel\u002F",[391],"リモート管理 Tunnel の作成",[100,4864,4865],{},[387,4866,4868],{"href":4485,"rel":4867},[391],"Tunnel のルーティングと CNAME",[100,4870,4871],{},[387,4872,4875],{"href":4873,"rel":4874},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fnetwork-ports\u002F",[391],"Cloudflare プロキシ対応ポート",[100,4877,4878],{},[387,4879,4881],{"href":4352,"rel":4880},[391],"Tunnel Token の権限とローテーション",[100,4883,4884],{},[387,4885,4888],{"href":4886,"rel":4887},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ftunnel\u002Ftroubleshooting\u002F",[391],"Tunnel の TLS トラブルシューティング",[744,4890,4891],{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":38,"searchDepth":748,"depth":748,"links":4893},[4894,4895,4902,4911,4918,4919,4920,4921],{"id":3371,"depth":276,"text":3371},{"id":3444,"depth":276,"text":3445,"children":4896},[4897,4898,4899,4900,4901],{"id":3448,"depth":748,"text":3449},{"id":3476,"depth":748,"text":3477},{"id":3512,"depth":748,"text":3513},{"id":3644,"depth":748,"text":3645},{"id":3671,"depth":748,"text":3672},{"id":3698,"depth":276,"text":3699,"children":4903},[4904,4905,4906,4907,4908,4909,4910],{"id":3712,"depth":748,"text":3713},{"id":3811,"depth":748,"text":3812},{"id":3877,"depth":748,"text":3878},{"id":3912,"depth":748,"text":3913},{"id":3946,"depth":748,"text":3947},{"id":4178,"depth":748,"text":4179},{"id":4258,"depth":748,"text":4259},{"id":4288,"depth":276,"text":4289,"children":4912},[4913,4914,4915,4916,4917],{"id":4305,"depth":748,"text":4306},{"id":4358,"depth":748,"text":4359},{"id":4469,"depth":748,"text":4470},{"id":4543,"depth":748,"text":4544},{"id":4565,"depth":748,"text":4566},{"id":618,"depth":276,"text":618},{"id":4730,"depth":276,"text":4730},{"id":4773,"depth":276,"text":4773},{"id":3185,"depth":276,"text":3185},"Lucky STUN で公開マッピングを取得し、Webhook で Cloudflare Single Redirect を更新して、固定ホスト名から最新の IP とポートへ一時転送する方法を解説します。303 の制約、TLS、アプリ互換性、Tunnel の代替案も扱います。",{},"\u002Fblog\u002Flucky-stun-cloudflare-tunnel-guide","2026-08-02",{"title":3322,"description":4922},"blog\u002Flucky-stun-cloudflare-tunnel-guide",[4929,3308,4930,3432,3310],"Lucky","Cloudflare Redirect Rules","sZ-MkF6S6FXroIu2SPuOIUIduKWCslgiF59fab_ODa8",{"id":4933,"title":4934,"accent":1300,"body":4935,"description":6515,"draft":775,"experience":776,"extension":777,"featured":778,"image":6516,"meta":6522,"navigation":778,"path":6523,"publishedAt":6524,"readingTime":1720,"seo":6525,"stem":6526,"tags":6527,"updatedAt":776,"video":776,"__hash__":6531,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fcloudflare-smtp-email-sending-guide.md","Cloudflare SMTP でメール送信：ダッシュボード設定から Nodemailer の本番運用まで",{"type":13,"value":4936,"toc":6496},[4937,4944,4947,4955,4974,4978,5062,5065,5079,5086,5089,5093,5096,5102,5105,5109,5115,5121,5126,5129,5148,5151,5170,5174,5177,5183,5189,5192,5214,5217,5285,5298,5301,5334,5338,5341,5344,5371,5374,5391,5394,5658,5664,5673,5676,5883,5901,5905,5908,5915,6007,6010,6013,6070,6073,6077,6080,6159,6170,6173,6182,6219,6222,6225,6228,6231,6309,6312,6318,6324,6330,6335,6355,6358,6361,6456,6459,6462,6465,6467,6493],[16,4938,4939,4940,4943],{},"Cloudflare Email Service は 2026 年に認証付き SMTP を追加しました。すでに Nodemailer、Python の ",[25,4941,4942],{},"smtplib","、PHPMailer、または SMTP 設定しか持たない製品を使っている場合、送信層全体を作り直さずに配信サービスを切り替えられます。",[16,4945,4946],{},"この記事は独立した SMTP チュートリアルです。Email Routing、受信処理、Email Workers の全体像を繰り返すのではなく、次の一つを完成させます。",[1355,4948,4949],{},[16,4950,4951,4954],{},[25,4952,4953],{},"smtp.mx.cloudflare.net:465"," に接続し、Cloudflare API Token で SMTPS 認証を行い、自分のドメインからトランザクションメールを送信する。",[16,4956,4957,4958,4961,4962,4967,4968,4973],{},"内容は ",[20,4959,4960],{},"2026 年 7 月 31 日","時点で確認できた Cloudflare Email Service Beta に基づきます。Beta の割り当てや画面は変わる可能性があるため、本番導入前に",[387,4963,4966],{"href":4964,"rel":4965},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Fsend-emails\u002F",[391],"公式の送信ガイド","と",[387,4969,4972],{"href":4970,"rel":4971},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fsmtp\u002F",[391],"SMTP API リファレンス","を確認してください。",[81,4975,4977],{"id":4976},"_1-分で分かる接続設定","1 分で分かる接続設定",[141,4979,4980,4989],{},[144,4981,4982],{},[147,4983,4984,4986],{},[150,4985,152],{},[150,4987,4988],{},"値",[160,4990,4991,5001,5011,5019,5030,5040,5054],{},[147,4992,4993,4996],{},[165,4994,4995],{},"SMTP ホスト",[165,4997,4998],{},[25,4999,5000],{},"smtp.mx.cloudflare.net",[147,5002,5003,5006],{},[165,5004,5005],{},"ポート",[165,5007,5008],{},[25,5009,5010],{},"465",[147,5012,5013,5016],{},[165,5014,5015],{},"暗号化",[165,5017,5018],{},"暗黙的 TLS、つまり SMTPS",[147,5020,5021,5024],{},[165,5022,5023],{},"ユーザー名",[165,5025,5026,5027],{},"固定文字列 ",[25,5028,5029],{},"api_token",[147,5031,5032,5034],{},[165,5033,522],{},[165,5035,5036,5039],{},[25,5037,5038],{},"Email Sending: Edit"," 権限を持つ Cloudflare API Token",[147,5041,5042,5045],{},[165,5043,5044],{},"認証方式",[165,5046,5047,5050,5051],{},[25,5048,5049],{},"AUTH PLAIN"," または ",[25,5052,5053],{},"AUTH LOGIN",[147,5055,5056,5059],{},[165,5057,5058],{},"From",[165,5060,5061],{},"Email Sending で有効化したドメインのアドレス",[16,5063,5064],{},"混同しやすい点が三つあります。",[408,5066,5067,5073,5076],{},[100,5068,5069,5070,5072],{},"ユーザー名は Cloudflare のメールアドレスや Account ID ではなく、常に ",[25,5071,5029],{}," です。",[100,5074,5075],{},"パスワードは Cloudflare へのログインパスワードではなく、専用 API Token です。",[100,5077,5078],{},"465 番ポートは接続開始時から TLS を使います。587 番ポートの STARTTLS 設定とは交換できません。",[16,5080,5081,5082,4302],{},"Cloudflare は現在、587 STARTTLS や 25 番ポートの送信リレーを提供していません。25 番ポートは受信用 Email Routing のためのものです。現在の境界は",[387,5083,5085],{"href":4970,"rel":5084},[391],"公式 SMTP リファレンス",[81,5087,5088],{"id":5088},"事前準備",[248,5090,5092],{"id":5091},"_1-送信ドメインを-cloudflare-dns-に置く","1. 送信ドメインを Cloudflare DNS に置く",[16,5094,5095],{},"Email Sending には Cloudflare DNS が必要です。ダッシュボードで次を開きます。",[33,5097,5100],{"className":5098,"code":5099,"language":233,"meta":38},[231],"Cloudflare Dashboard\n→ Compute\n→ Email Service\n→ Email Sending\n",[25,5101,5099],{"__ignoreMap":38},[16,5103,5104],{},"ドメインを選択してオンボーディングを完了します。Cloudflare は送信者認証、DKIM、バウンス処理に必要な DNS レコードを設定します。ドメインが「設定済み」になってから SMTP を調査してください。ログインに成功しても、送信ドメインが許可されていなければ拒否されます。",[248,5106,5108],{"id":5107},"_2-最小権限の-api-token-を作る","2. 最小権限の API Token を作る",[16,5110,5111,5112,5114],{},"ドメインの「接続」ページで SMTP を選択します。ダッシュボードには ",[25,5113,5038],{}," 権限を持つ API Token の作成ボタンがあります。",[16,5116,5117],{},[1994,5118],{"alt":5119,"src":5120},"ホスト、ポート、ユーザー名、cURL の例を表示する Cloudflare Email Service の SMTP 接続ページ","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-curl-v1.webp",[16,5122,5123],{},[2001,5124,5125],{},"2026 年 7 月 31 日に取得した画面です。Cloudflare の公開設定値と環境変数のプレースホルダーのみが含まれ、実際の Token は作成、表示、掲載していません。",[16,5127,5128],{},"アカウント所有の Token を優先し、可能な限り範囲を絞ります。",[97,5130,5131,5134,5139,5142,5145],{},[100,5132,5133],{},"メールを送る Cloudflare アカウントだけを許可する。",[100,5135,5136,5138],{},[25,5137,5038],{}," だけを付与する。",[100,5140,5141],{},"適切な有効期限を設定する。",[100,5143,5144],{},"デプロイ先の送信元 IP が固定なら、クライアント IP 制限も追加する。",[100,5146,5147],{},"アプリごとに Token を分け、個別に監査・失効できるようにする。",[16,5149,5150],{},"完全な Token は作成時に一度だけ表示されます。サーバー側の Secret または実行時環境変数へ保存してください。次の場所には置けません。",[97,5152,5153,5156,5161,5164,5167],{},[100,5154,5155],{},"Git、Markdown、ビルドログ",[100,5157,5158],{},[25,5159,5160],{},"NUXT_PUBLIC_*",[100,5162,5163],{},"ブラウザー JavaScript",[100,5165,5166],{},"エラー時に出力する SMTP 設定オブジェクト",[100,5168,5169],{},"複数の無関係なアプリで共有する長期 Token",[81,5171,5173],{"id":5172},"まず-curl-で経路を検証する","まず cURL で経路を検証する",[16,5175,5176],{},"最小の cURL テストを先に行うと、Cloudflare 側の設定エラーとフレームワーク統合エラーを分離できます。",[16,5178,5179,5182],{},[25,5180,5181],{},"mail.txt"," を作成します。",[33,5184,5187],{"className":5185,"code":5186,"language":233,"meta":38},[231],"From: Jackie Moon \u003Cnoreply@sparkles-editor.com>\nTo: recipient@example.com\nSubject: Cloudflare SMTP test\nMIME-Version: 1.0\nContent-Type: text\u002Fplain; charset=UTF-8\n\nHello,\n\nThis message was delivered through Cloudflare Email Service SMTP.\n",[25,5188,5186],{"__ignoreMap":38},[16,5190,5191],{},"Token の値をシェル履歴に残さず、現在のプロセスへ読み込みます。",[33,5193,5195],{"className":35,"code":5194,"language":37,"meta":38,"style":38},"read -s CF_EMAIL_SMTP_TOKEN\nexport CF_EMAIL_SMTP_TOKEN\n",[25,5196,5197,5207],{"__ignoreMap":38},[42,5198,5199,5202,5204],{"class":44,"line":45},[42,5200,5201],{"class":52},"read",[42,5203,286],{"class":52},[42,5205,5206],{"class":56}," CF_EMAIL_SMTP_TOKEN\n",[42,5208,5209,5212],{"class":44,"line":276},[42,5210,5211],{"class":279},"export",[42,5213,5206],{"class":1788},[16,5215,5216],{},"送信します。",[33,5218,5220],{"className":35,"code":5219,"language":37,"meta":38,"style":38},"curl --url \"smtps:\u002F\u002Fsmtp.mx.cloudflare.net:465\" \\\n  --ssl-reqd \\\n  --user \"api_token:${CF_EMAIL_SMTP_TOKEN}\" \\\n  --mail-from \"noreply@sparkles-editor.com\" \\\n  --mail-rcpt \"recipient@example.com\" \\\n  --upload-file mail.txt\n",[25,5221,5222,5234,5241,5257,5267,5277],{"__ignoreMap":38},[42,5223,5224,5226,5229,5232],{"class":44,"line":45},[42,5225,264],{"class":48},[42,5227,5228],{"class":52}," --url",[42,5230,5231],{"class":56}," \"smtps:\u002F\u002Fsmtp.mx.cloudflare.net:465\"",[42,5233,273],{"class":52},[42,5235,5236,5239],{"class":44,"line":276},[42,5237,5238],{"class":52},"  --ssl-reqd",[42,5240,273],{"class":52},[42,5242,5243,5246,5249,5252,5255],{"class":44,"line":748},[42,5244,5245],{"class":52},"  --user",[42,5247,5248],{"class":56}," \"api_token:${",[42,5250,5251],{"class":1788},"CF_EMAIL_SMTP_TOKEN",[42,5253,5254],{"class":56},"}\"",[42,5256,273],{"class":52},[42,5258,5259,5262,5265],{"class":44,"line":1590},[42,5260,5261],{"class":52},"  --mail-from",[42,5263,5264],{"class":56}," \"noreply@sparkles-editor.com\"",[42,5266,273],{"class":52},[42,5268,5269,5272,5275],{"class":44,"line":1609},[42,5270,5271],{"class":52},"  --mail-rcpt",[42,5273,5274],{"class":56}," \"recipient@example.com\"",[42,5276,273],{"class":52},[42,5278,5279,5282],{"class":44,"line":1624},[42,5280,5281],{"class":52},"  --upload-file",[42,5283,5284],{"class":56}," mail.txt\n",[16,5286,5287,5288,5291,5292,5297],{},"成功すると SMTP は ",[25,5289,5290],{},"250"," を返します。応答に含まれる Message-ID を保存すると、Email Service ダッシュボードの送信ログと関連付けられます。",[387,5293,5296],{"href":5294,"rel":5295},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fexamples\u002Femail-sending\u002Fsmtp\u002F",[391],"Cloudflare の公式 cURL 例","とも比較してください。",[16,5299,5300],{},"ここで失敗した場合、まだ Nodemailer を変更しないでください。",[97,5302,5303,5312,5318,5324],{},[100,5304,5305,5308,5309,5311],{},[25,5306,5307],{},"535","：Token が無効、期限切れ、または ",[25,5310,5038],{}," がない。",[100,5313,5314,5317],{},[25,5315,5316],{},"550","：送信アドレスまたはドメインが許可されていない。",[100,5319,5320,5323],{},[25,5321,5322],{},"552","：メッセージが大きすぎる。",[100,5325,5326,5329,5330,5333],{},[25,5327,5328],{},"421"," \u002F ",[25,5331,5332],{},"451","：一時的な障害またはレート制限。時間を置いて再試行する。",[81,5335,5337],{"id":5336},"nodejs-から-nodemailer-を使う","Node.js から Nodemailer を使う",[16,5339,5340],{},"この構成は Node.js、Docker、VPS、一般的な Serverless Function、または Node 互換環境で動く Nitro サーバー向けです。",[16,5342,5343],{},"依存関係を追加します。",[33,5345,5347],{"className":35,"code":5346,"language":37,"meta":38,"style":38},"pnpm add nodemailer\npnpm add -D @types\u002Fnodemailer\n",[25,5348,5349,5360],{"__ignoreMap":38},[42,5350,5351,5354,5357],{"class":44,"line":45},[42,5352,5353],{"class":48},"pnpm",[42,5355,5356],{"class":56}," add",[42,5358,5359],{"class":56}," nodemailer\n",[42,5361,5362,5364,5366,5368],{"class":44,"line":276},[42,5363,5353],{"class":48},[42,5365,5356],{"class":56},[42,5367,2852],{"class":52},[42,5369,5370],{"class":56}," @types\u002Fnodemailer\n",[16,5372,5373],{},"サーバー側の環境変数を定義します。",[33,5375,5379],{"className":5376,"code":5377,"language":5378,"meta":38,"style":38},"language-dotenv shiki shiki-themes github-dark github-dark","CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret\nSMTP_FROM=\"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n","dotenv",[25,5380,5381,5386],{"__ignoreMap":38},[42,5382,5383],{"class":44,"line":45},[42,5384,5385],{},"CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret\n",[42,5387,5388],{"class":44,"line":276},[42,5389,5390],{},"SMTP_FROM=\"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[16,5392,5393],{},"Transport を作成します。",[33,5395,5399],{"className":5396,"code":5397,"language":5398,"meta":38,"style":38},"language-ts shiki shiki-themes github-dark github-dark","import nodemailer from 'nodemailer'\n\nconst transporter = nodemailer.createTransport({\n  host: 'smtp.mx.cloudflare.net',\n  port: 465,\n  secure: true,\n  auth: {\n    user: 'api_token',\n    pass: process.env.CF_EMAIL_SMTP_TOKEN,\n  },\n  connectionTimeout: 10_000,\n  greetingTimeout: 10_000,\n  socketTimeout: 30_000,\n})\n\nconst info = await transporter.sendMail({\n  from: process.env.SMTP_FROM,\n  to: 'recipient@example.com',\n  subject: 'Welcome to Signal Log',\n  text: 'Your account is ready.',\n  html: '\u003Cp>Your account is ready.\u003C\u002Fp>',\n})\n\nconsole.info('Email accepted', {\n  messageId: info.messageId,\n  accepted: info.accepted.length,\n  rejected: info.rejected.length,\n})\n","ts",[25,5400,5401,5415,5419,5439,5449,5458,5467,5472,5482,5491,5496,5506,5515,5525,5530,5534,5554,5564,5574,5584,5594,5604,5608,5612,5629,5634,5644,5653],{"__ignoreMap":38},[42,5402,5403,5406,5409,5412],{"class":44,"line":45},[42,5404,5405],{"class":279},"import",[42,5407,5408],{"class":1788}," nodemailer ",[42,5410,5411],{"class":279},"from",[42,5413,5414],{"class":56}," 'nodemailer'\n",[42,5416,5417],{"class":44,"line":276},[42,5418,1587],{"emptyLinePlaceholder":778},[42,5420,5421,5424,5427,5430,5433,5436],{"class":44,"line":748},[42,5422,5423],{"class":279},"const",[42,5425,5426],{"class":52}," transporter",[42,5428,5429],{"class":279}," =",[42,5431,5432],{"class":1788}," nodemailer.",[42,5434,5435],{"class":48},"createTransport",[42,5437,5438],{"class":1788},"({\n",[42,5440,5441,5444,5447],{"class":44,"line":1590},[42,5442,5443],{"class":1788},"  host: ",[42,5445,5446],{"class":56},"'smtp.mx.cloudflare.net'",[42,5448,3988],{"class":1788},[42,5450,5451,5454,5456],{"class":44,"line":1609},[42,5452,5453],{"class":1788},"  port: ",[42,5455,5010],{"class":52},[42,5457,3988],{"class":1788},[42,5459,5460,5463,5465],{"class":44,"line":1624},[42,5461,5462],{"class":1788},"  secure: ",[42,5464,4010],{"class":52},[42,5466,3988],{"class":1788},[42,5468,5469],{"class":44,"line":1633},[42,5470,5471],{"class":1788},"  auth: {\n",[42,5473,5474,5477,5480],{"class":44,"line":1646},[42,5475,5476],{"class":1788},"    user: ",[42,5478,5479],{"class":56},"'api_token'",[42,5481,3988],{"class":1788},[42,5483,5484,5487,5489],{"class":44,"line":1651},[42,5485,5486],{"class":1788},"    pass: process.env.",[42,5488,5251],{"class":52},[42,5490,3988],{"class":1788},[42,5492,5493],{"class":44,"line":1674},[42,5494,5495],{"class":1788},"  },\n",[42,5497,5498,5501,5504],{"class":44,"line":1680},[42,5499,5500],{"class":1788},"  connectionTimeout: ",[42,5502,5503],{"class":52},"10_000",[42,5505,3988],{"class":1788},[42,5507,5508,5511,5513],{"class":44,"line":1686},[42,5509,5510],{"class":1788},"  greetingTimeout: ",[42,5512,5503],{"class":52},[42,5514,3988],{"class":1788},[42,5516,5517,5520,5523],{"class":44,"line":788},[42,5518,5519],{"class":1788},"  socketTimeout: ",[42,5521,5522],{"class":52},"30_000",[42,5524,3988],{"class":1788},[42,5526,5527],{"class":44,"line":1697},[42,5528,5529],{"class":1788},"})\n",[42,5531,5532],{"class":44,"line":1703},[42,5533,1587],{"emptyLinePlaceholder":778},[42,5535,5536,5538,5541,5543,5546,5549,5552],{"class":44,"line":1709},[42,5537,5423],{"class":279},[42,5539,5540],{"class":52}," info",[42,5542,5429],{"class":279},[42,5544,5545],{"class":279}," await",[42,5547,5548],{"class":1788}," transporter.",[42,5550,5551],{"class":48},"sendMail",[42,5553,5438],{"class":1788},[42,5555,5556,5559,5562],{"class":44,"line":1715},[42,5557,5558],{"class":1788},"  from: process.env.",[42,5560,5561],{"class":52},"SMTP_FROM",[42,5563,3988],{"class":1788},[42,5565,5566,5569,5572],{"class":44,"line":1720},[42,5567,5568],{"class":1788},"  to: ",[42,5570,5571],{"class":56},"'recipient@example.com'",[42,5573,3988],{"class":1788},[42,5575,5576,5579,5582],{"class":44,"line":1729},[42,5577,5578],{"class":1788},"  subject: ",[42,5580,5581],{"class":56},"'Welcome to Signal Log'",[42,5583,3988],{"class":1788},[42,5585,5586,5589,5592],{"class":44,"line":1742},[42,5587,5588],{"class":1788},"  text: ",[42,5590,5591],{"class":56},"'Your account is ready.'",[42,5593,3988],{"class":1788},[42,5595,5596,5599,5602],{"class":44,"line":1756},[42,5597,5598],{"class":1788},"  html: ",[42,5600,5601],{"class":56},"'\u003Cp>Your account is ready.\u003C\u002Fp>'",[42,5603,3988],{"class":1788},[42,5605,5606],{"class":44,"line":2239},[42,5607,5529],{"class":1788},[42,5609,5610],{"class":44,"line":2249},[42,5611,1587],{"emptyLinePlaceholder":778},[42,5613,5614,5617,5620,5623,5626],{"class":44,"line":2259},[42,5615,5616],{"class":1788},"console.",[42,5618,5619],{"class":48},"info",[42,5621,5622],{"class":1788},"(",[42,5624,5625],{"class":56},"'Email accepted'",[42,5627,5628],{"class":1788},", {\n",[42,5630,5631],{"class":44,"line":2269},[42,5632,5633],{"class":1788},"  messageId: info.messageId,\n",[42,5635,5636,5639,5642],{"class":44,"line":2274},[42,5637,5638],{"class":1788},"  accepted: info.accepted.",[42,5640,5641],{"class":52},"length",[42,5643,3988],{"class":1788},[42,5645,5646,5649,5651],{"class":44,"line":2282},[42,5647,5648],{"class":1788},"  rejected: info.rejected.",[42,5650,5641],{"class":52},[42,5652,3988],{"class":1788},[42,5654,5656],{"class":44,"line":5655},28,[42,5657,5529],{"class":1788},[16,5659,5660],{},[1994,5661],{"alt":5662,"src":5663},"パスワードを環境変数から読み込む Cloudflare ダッシュボードの Nodemailer 例","\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fdashboard-smtp-node-v1.webp",[16,5665,5666],{},[2001,5667,5668,5669,5672],{},"Cloudflare の画面例は ",[25,5670,5671],{},"process.env.CLOUDFLARE_API_TOKEN"," を使っています。プロジェクトではより具体的な変数名にできますが、値は必ずサーバー側に置きます。",[16,5674,5675],{},"本番コードには envelope の境界も必要です。",[33,5677,5679],{"className":5396,"code":5678,"language":5398,"meta":38,"style":38},"const allowedFrom = new Set([\n  'noreply@sparkles-editor.com',\n  'support@sparkles-editor.com',\n])\n\nfunction assertEmailEnvelope(from: string, to: string) {\n  if (!allowedFrom.has(from)) {\n    throw new Error('Sender is not allowed')\n  }\n\n  if (!\u002F^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\u002F.test(to)) {\n    throw new Error('Recipient is invalid')\n  }\n}\n",[25,5680,5681,5699,5706,5713,5718,5722,5754,5774,5792,5796,5800,5860,5875,5879],{"__ignoreMap":38},[42,5682,5683,5685,5688,5690,5693,5696],{"class":44,"line":45},[42,5684,5423],{"class":279},[42,5686,5687],{"class":52}," allowedFrom",[42,5689,5429],{"class":279},[42,5691,5692],{"class":279}," new",[42,5694,5695],{"class":48}," Set",[42,5697,5698],{"class":1788},"([\n",[42,5700,5701,5704],{"class":44,"line":276},[42,5702,5703],{"class":56},"  'noreply@sparkles-editor.com'",[42,5705,3988],{"class":1788},[42,5707,5708,5711],{"class":44,"line":748},[42,5709,5710],{"class":56},"  'support@sparkles-editor.com'",[42,5712,3988],{"class":1788},[42,5714,5715],{"class":44,"line":1590},[42,5716,5717],{"class":1788},"])\n",[42,5719,5720],{"class":44,"line":1609},[42,5721,1587],{"emptyLinePlaceholder":778},[42,5723,5724,5727,5730,5732,5735,5738,5741,5744,5747,5749,5751],{"class":44,"line":1624},[42,5725,5726],{"class":279},"function",[42,5728,5729],{"class":48}," assertEmailEnvelope",[42,5731,5622],{"class":1788},[42,5733,5411],{"class":5734},"s-3mD",[42,5736,5737],{"class":279},":",[42,5739,5740],{"class":52}," string",[42,5742,5743],{"class":1788},", ",[42,5745,5746],{"class":5734},"to",[42,5748,5737],{"class":279},[42,5750,5740],{"class":52},[42,5752,5753],{"class":1788},") {\n",[42,5755,5756,5759,5762,5765,5768,5771],{"class":44,"line":1633},[42,5757,5758],{"class":279},"  if",[42,5760,5761],{"class":1788}," (",[42,5763,5764],{"class":279},"!",[42,5766,5767],{"class":1788},"allowedFrom.",[42,5769,5770],{"class":48},"has",[42,5772,5773],{"class":1788},"(from)) {\n",[42,5775,5776,5779,5781,5784,5786,5789],{"class":44,"line":1646},[42,5777,5778],{"class":279},"    throw",[42,5780,5692],{"class":279},[42,5782,5783],{"class":48}," Error",[42,5785,5622],{"class":1788},[42,5787,5788],{"class":56},"'Sender is not allowed'",[42,5790,5791],{"class":1788},")\n",[42,5793,5794],{"class":44,"line":1651},[42,5795,4111],{"class":1788},[42,5797,5798],{"class":44,"line":1674},[42,5799,1587],{"emptyLinePlaceholder":778},[42,5801,5802,5804,5806,5808,5811,5814,5817,5819,5822,5825,5829,5831,5833,5835,5837,5841,5843,5845,5847,5850,5852,5854,5857],{"class":44,"line":1680},[42,5803,5758],{"class":279},[42,5805,5761],{"class":1788},[42,5807,5764],{"class":279},[42,5809,5810],{"class":56},"\u002F",[42,5812,5813],{"class":279},"^",[42,5815,5816],{"class":52},"[",[42,5818,5813],{"class":279},[42,5820,5821],{"class":52},"\\s@]",[42,5823,5824],{"class":279},"+",[42,5826,5828],{"class":5827},"se7xv","@",[42,5830,5816],{"class":52},[42,5832,5813],{"class":279},[42,5834,5821],{"class":52},[42,5836,5824],{"class":279},[42,5838,5840],{"class":5839},"s-Lb2","\\.",[42,5842,5816],{"class":52},[42,5844,5813],{"class":279},[42,5846,5821],{"class":52},[42,5848,5849],{"class":279},"+$",[42,5851,5810],{"class":56},[42,5853,1782],{"class":1788},[42,5855,5856],{"class":48},"test",[42,5858,5859],{"class":1788},"(to)) {\n",[42,5861,5862,5864,5866,5868,5870,5873],{"class":44,"line":1686},[42,5863,5778],{"class":279},[42,5865,5692],{"class":279},[42,5867,5783],{"class":48},[42,5869,5622],{"class":1788},[42,5871,5872],{"class":56},"'Recipient is invalid'",[42,5874,5791],{"class":1788},[42,5876,5877],{"class":44,"line":788},[42,5878,4111],{"class":1788},[42,5880,5881],{"class":44,"line":1697},[42,5882,4116],{"class":1788},[16,5884,5885,5886,437,5888,437,5890,5893,5894,511,5897,5900],{},"任意の ",[25,5887,5411],{},[25,5889,5746],{},[25,5891,5892],{},"subject","、HTML を受け取る公開 API は作らないでください。オープンリレーやフィッシング送信に悪用されます。安全な API は ",[25,5895,5896],{},"userId",[25,5898,5899],{},"templateId"," のような業務パラメータだけを受け取り、サーバーが検証済みの宛先を取得し、許可済みテンプレートから内容を生成します。",[81,5902,5904],{"id":5903},"nuxt-が-cloudflare-workers-上で動く場合","Nuxt が Cloudflare Workers 上で動く場合",[16,5906,5907],{},"実行環境を区別する必要があります。",[16,5909,5910,5911,5914],{},"Nodemailer は通常の Node SMTP Socket を対象にしたライブラリです。すでに Cloudflare Workers 上で動く Nuxt アプリは、通常 ",[25,5912,5913],{},"send_email"," binding を使う方が自然です。",[33,5916,5918],{"className":5396,"code":5917,"language":5398,"meta":38,"style":38},"interface Env {\n  EMAIL: SendEmail\n}\n\nawait env.EMAIL.send({\n  from: 'noreply@sparkles-editor.com',\n  to: 'recipient@example.com',\n  subject: 'Verification code',\n  text: 'Your code is 123456',\n})\n",[25,5919,5920,5931,5941,5945,5949,5967,5977,5985,5994,6003],{"__ignoreMap":38},[42,5921,5922,5925,5928],{"class":44,"line":45},[42,5923,5924],{"class":279},"interface",[42,5926,5927],{"class":48}," Env",[42,5929,5930],{"class":1788}," {\n",[42,5932,5933,5936,5938],{"class":44,"line":276},[42,5934,5935],{"class":5734},"  EMAIL",[42,5937,5737],{"class":279},[42,5939,5940],{"class":48}," SendEmail\n",[42,5942,5943],{"class":44,"line":748},[42,5944,4116],{"class":1788},[42,5946,5947],{"class":44,"line":1590},[42,5948,1587],{"emptyLinePlaceholder":778},[42,5950,5951,5954,5957,5960,5962,5965],{"class":44,"line":1609},[42,5952,5953],{"class":279},"await",[42,5955,5956],{"class":1788}," env.",[42,5958,5959],{"class":52},"EMAIL",[42,5961,1782],{"class":1788},[42,5963,5964],{"class":48},"send",[42,5966,5438],{"class":1788},[42,5968,5969,5972,5975],{"class":44,"line":1624},[42,5970,5971],{"class":1788},"  from: ",[42,5973,5974],{"class":56},"'noreply@sparkles-editor.com'",[42,5976,3988],{"class":1788},[42,5978,5979,5981,5983],{"class":44,"line":1633},[42,5980,5568],{"class":1788},[42,5982,5571],{"class":56},[42,5984,3988],{"class":1788},[42,5986,5987,5989,5992],{"class":44,"line":1646},[42,5988,5578],{"class":1788},[42,5990,5991],{"class":56},"'Verification code'",[42,5993,3988],{"class":1788},[42,5995,5996,5998,6001],{"class":44,"line":1651},[42,5997,5588],{"class":1788},[42,5999,6000],{"class":56},"'Your code is 123456'",[42,6002,3988],{"class":1788},[42,6004,6005],{"class":44,"line":1674},[42,6006,5529],{"class":1788},[16,6008,6009],{},"Worker に SMTP API Token を保存せずに済み、binding 自体が権限境界になります。",[16,6011,6012],{},"選択基準は次のとおりです。",[141,6014,6015,6025],{},[144,6016,6017],{},[147,6018,6019,6022],{},[150,6020,6021],{},"状況",[150,6023,6024],{},"推奨する入口",[160,6026,6027,6038,6046,6054,6062],{},[147,6028,6029,6032],{},[165,6030,6031],{},"Nuxt \u002F API が Cloudflare Workers 上にある",[165,6033,6034,6035,6037],{},"Workers の ",[25,6036,5913],{}," binding",[147,6039,6040,6043],{},[165,6041,6042],{},"外部サービスが HTTPS と Token 管理に適している",[165,6044,6045],{},"REST API",[147,6047,6048,6051],{},[165,6049,6050],{},"既存の Node、Python、PHP アプリが SMTP に対応済み",[165,6052,6053],{},"認証付き SMTP",[147,6055,6056,6059],{},[165,6057,6058],{},"プリンター、旧 CMS、外部製品が SMTP 設定しか持たない",[165,6060,6061],{},"465 SMTPS 対応を確認して SMTP",[147,6063,6064,6067],{},[165,6065,6066],{},"ブラウザーから直接送る",[165,6068,6069],{},"禁止。管理されたサーバー API を使う",[16,6071,6072],{},"三つのサーバー側入口は Cloudflare の配信、ログ、ドメイン認証へ合流します。SMTP の価値は互換性であり、すべての新規サービスを Socket に戻すことではありません。",[81,6074,6076],{"id":6075},"python-の最小例","Python の最小例",[16,6078,6079],{},"Python の標準ライブラリだけで送信できます。",[33,6081,6085],{"className":6082,"code":6083,"language":6084,"meta":38,"style":38},"language-python shiki shiki-themes github-dark github-dark","import os\nimport smtplib\nfrom email.message import EmailMessage\n\ntoken = os.environ[\"CF_EMAIL_SMTP_TOKEN\"]\n\nmessage = EmailMessage()\nmessage[\"From\"] = \"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\nmessage[\"To\"] = \"recipient@example.com\"\nmessage[\"Subject\"] = \"Cloudflare SMTP test\"\nmessage.set_content(\"This email was sent over implicit TLS.\")\n\nwith smtplib.SMTP_SSL(\"smtp.mx.cloudflare.net\", 465, timeout=30) as smtp:\n    smtp.login(\"api_token\", token)\n    smtp.send_message(message)\n","python",[25,6086,6087,6092,6097,6102,6106,6111,6115,6120,6125,6130,6135,6140,6144,6149,6154],{"__ignoreMap":38},[42,6088,6089],{"class":44,"line":45},[42,6090,6091],{},"import os\n",[42,6093,6094],{"class":44,"line":276},[42,6095,6096],{},"import smtplib\n",[42,6098,6099],{"class":44,"line":748},[42,6100,6101],{},"from email.message import EmailMessage\n",[42,6103,6104],{"class":44,"line":1590},[42,6105,1587],{"emptyLinePlaceholder":778},[42,6107,6108],{"class":44,"line":1609},[42,6109,6110],{},"token = os.environ[\"CF_EMAIL_SMTP_TOKEN\"]\n",[42,6112,6113],{"class":44,"line":1624},[42,6114,1587],{"emptyLinePlaceholder":778},[42,6116,6117],{"class":44,"line":1633},[42,6118,6119],{},"message = EmailMessage()\n",[42,6121,6122],{"class":44,"line":1646},[42,6123,6124],{},"message[\"From\"] = \"Jackie Moon \u003Cnoreply@sparkles-editor.com>\"\n",[42,6126,6127],{"class":44,"line":1651},[42,6128,6129],{},"message[\"To\"] = \"recipient@example.com\"\n",[42,6131,6132],{"class":44,"line":1674},[42,6133,6134],{},"message[\"Subject\"] = \"Cloudflare SMTP test\"\n",[42,6136,6137],{"class":44,"line":1680},[42,6138,6139],{},"message.set_content(\"This email was sent over implicit TLS.\")\n",[42,6141,6142],{"class":44,"line":1686},[42,6143,1587],{"emptyLinePlaceholder":778},[42,6145,6146],{"class":44,"line":788},[42,6147,6148],{},"with smtplib.SMTP_SSL(\"smtp.mx.cloudflare.net\", 465, timeout=30) as smtp:\n",[42,6150,6151],{"class":44,"line":1697},[42,6152,6153],{},"    smtp.login(\"api_token\", token)\n",[42,6155,6156],{"class":44,"line":1703},[42,6157,6158],{},"    smtp.send_message(message)\n",[16,6160,6161,6162,6165,6166,6169],{},"重要なのは ",[25,6163,6164],{},"SMTP_SSL"," であり、平文接続の後に ",[25,6167,6168],{},"starttls()"," を呼ぶ構成ではありません。",[81,6171,6172],{"id":6172},"制限と添付ファイル",[16,6174,6175,6176,6181],{},"現在の",[387,6177,6180],{"href":6178,"rel":6179},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Flimits\u002F",[391],"Email Service プラットフォーム制限","は次のとおりです。",[97,6183,6184,6187,6194,6197,6200,6207,6213,6216],{},[100,6185,6186],{},"1 通あたりの宛先合計は最大 50。",[100,6188,6189,6190,6193],{},"SMTP セッションで受け付ける ",[25,6191,6192],{},"RCPT TO"," は最大 50 回。",[100,6195,6196],{},"通常のメッセージ総容量は最大 5 MiB。",[100,6198,6199],{},"検証済み宛先へのメッセージは最大 25 MiB。",[100,6201,6202,6203,6206],{},"SMTP ",[25,6204,6205],{},"AUTH"," のタイムアウトは 30 秒。",[100,6208,6202,6209,6212],{},[25,6210,6211],{},"DATA"," のタイムアウトは 300 秒。",[100,6214,6215],{},"カスタムヘッダー合計は 16 KiB。",[100,6217,6218],{},"Subject は 998 文字まで。",[16,6220,6221],{},"総容量には MIME エンコード後の本文と添付ファイルが含まれます。Base64 ではバイナリが約 3 分の 1 増えることが多いため、4.9 MiB のファイルが 5 MiB のメールに収まるとは限りません。",[16,6223,6224],{},"ダッシュボードの日次割り当ては、アカウント、Beta 段階、利用状況によって変わる可能性があります。一度見た値をハードコードせず、現在の画面を確認して送信キューに余裕を残します。",[81,6226,6227],{"id":6227},"エラー処理と再試行",[16,6229,6230],{},"SMTP 応答は再試行すべきかを表しています。",[141,6232,6233,6245],{},[144,6234,6235],{},[147,6236,6237,6240,6242],{},[150,6238,6239],{},"応答",[150,6241,158],{},[150,6243,6244],{},"対応",[160,6246,6247,6259,6273,6285,6297],{},[147,6248,6249,6253,6256],{},[165,6250,6251],{},[25,6252,5290],{},[165,6254,6255],{},"Cloudflare が受理した",[165,6257,6258],{},"Message-ID を保存し、重複送信しない",[147,6260,6261,6267,6270],{},[165,6262,6263,5329,6265],{},[25,6264,5328],{},[25,6266,5332],{},[165,6268,6269],{},"一時障害または制限",[165,6271,6272],{},"ジッター付き指数バックオフ",[147,6274,6275,6279,6282],{},[165,6276,6277],{},[25,6278,5307],{},[165,6280,6281],{},"認証失敗",[165,6283,6284],{},"自動再試行を止め、Token を確認",[147,6286,6287,6291,6294],{},[165,6288,6289],{},[25,6290,5316],{},[165,6292,6293],{},"送信者、ドメイン、宛先ポリシーによる拒否",[165,6295,6296],{},"設定またはデータを直してから送る",[147,6298,6299,6303,6306],{},[165,6300,6301],{},[25,6302,5322],{},[165,6304,6305],{},"メッセージが大きすぎる",[165,6307,6308],{},"本文または添付を減らす",[16,6310,6311],{},"再試行は 30 秒、2 分、10 分、30 分のようにし、最大回数を固定します。業務上の送信には冪等キーが必要です。",[33,6313,6316],{"className":6314,"code":6315,"language":233,"meta":38},[231],"password-reset:{userId}:{resetRequestId}\ninvoice:{invoiceId}:issued\n",[25,6317,6315],{"__ignoreMap":38},[16,6319,6320,6321,6323],{},"一時エラーはキューへ、恒久エラーは失敗または確認状態へ送ります。すべての非 ",[25,6322,5290],{}," を即時再試行したり、ネットワークの揺れで同じ認証コードを十回送ったりしてはいけません。",[81,6325,6327,6329],{"id":6326},"_250-は最終到達ではない",[25,6328,5290],{}," は最終到達ではない",[16,6331,6332,6334],{},[25,6333,5290],{}," は Cloudflare が受理したという意味で、最終受信箱への到着を保証しません。本番環境では次を監視します。",[97,6336,6337,6340,6343,6346,6349,6352],{},[100,6338,6339],{},"Delivery、Bounce、Deferred、Suppression",[100,6341,6342],{},"Gmail、Outlook、QQ メールなど主要宛先の結果",[100,6344,6345],{},"From、Return-Path、DKIM、DMARC の整合",[100,6347,6348],{},"ハードバウンスした宛先の即時停止",[100,6350,6351],{},"苦情率と無効アドレスの増加",[100,6353,6354],{},"認証コードとパスワード再設定メールの明確な有効期限",[16,6356,6357],{},"トランザクションメールには安定して識別できる送信者を使い、テキスト版と HTML 版を用意します。Reply-To を偽装せず、認証用経路で広告を大量送信せず、ユーザー入力をメールヘッダーへ直接入れないでください。",[81,6359,6360],{"id":6360},"本番チェックリスト",[97,6362,6365,6374,6380,6388,6397,6403,6409,6415,6421,6432,6444,6450],{"className":6363},[6364],"contains-task-list",[100,6366,6369,6373],{"className":6367},[6368],"task-list-item",[6370,6371],"input",{"disabled":778,"type":6372},"checkbox"," 送信ドメインが Email Sending で設定済み",[100,6375,6377,6379],{"className":6376},[6368],[6370,6378],{"disabled":778,"type":6372}," 465 番ポートと暗黙的 TLS を使用",[100,6381,6383,6385,6386],{"className":6382},[6368],[6370,6384],{"disabled":778,"type":6372}," ユーザー名が正確に ",[25,6387,5029],{},[100,6389,6391,6393,6394,6396],{"className":6390},[6368],[6370,6392],{"disabled":778,"type":6372}," Token の権限が ",[25,6395,5038],{}," のみ",[100,6398,6400,6402],{"className":6399},[6368],[6370,6401],{"disabled":778,"type":6372}," Token がサーバー Secret にあり、Git、ログ、フロントにない",[100,6404,6406,6408],{"className":6405},[6368],[6370,6407],{"disabled":778,"type":6372}," From はサーバー側 allowlist から選ぶ",[100,6410,6412,6414],{"className":6411},[6368],[6370,6413],{"disabled":778,"type":6372}," 宛先を検証、重複排除、長さ制限している",[100,6416,6418,6420],{"className":6417},[6368],[6370,6419],{"disabled":778,"type":6372}," HTML は固定テンプレートで、テキスト版もある",[100,6422,6424,6426,6427,5329,6429,6431],{"className":6423},[6368],[6370,6425],{"disabled":778,"type":6372}," ",[25,6428,5328],{},[25,6430,5332],{}," はジッター付き指数バックオフ",[100,6433,6435,6426,6437,5329,6439,5329,6441,6443],{"className":6434},[6368],[6370,6436],{"disabled":778,"type":6372},[25,6438,5307],{},[25,6440,5316],{},[25,6442,5322],{}," を無条件再試行しない",[100,6445,6447,6449],{"className":6446},[6368],[6370,6448],{"disabled":778,"type":6372}," Message-ID、テンプレート名、冪等キーを記録し、Token は記録しない",[100,6451,6453,6455],{"className":6452},[6368],[6370,6454],{"disabled":778,"type":6372}," バウンス、抑制、苦情、日次割り当てを監視",[81,6457,6458],{"id":6458},"最後の選択",[16,6460,6461],{},"安定稼働中の Node、Python、PHP アプリでは、Cloudflare SMTP の利点は明快です。ホスト、ポート、認証情報を変更するだけで Cloudflare の配信とログへ接続できます。",[16,6463,6464],{},"Workers ネイティブのアプリなら、従来サーバーを再現する必要はありません。Email binding を優先してください。SMTP は既存エコシステムへの橋であり、唯一の入口ではありません。",[248,6466,3185],{"id":3185},[97,6468,6469,6475,6481,6487],{},[100,6470,6471],{},[387,6472,6474],{"href":4964,"rel":6473},[391],"Cloudflare Email Service：メール送信を始める",[100,6476,6477],{},[387,6478,6480],{"href":4970,"rel":6479},[391],"Cloudflare Email Service：SMTP API リファレンス",[100,6482,6483],{},[387,6484,6486],{"href":5294,"rel":6485},[391],"Cloudflare Email Service：SMTP の例",[100,6488,6489],{},[387,6490,6492],{"href":6178,"rel":6491},[391],"Cloudflare Email Service：プラットフォーム制限",[744,6494,6495],{},"html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .se7xv, html code.shiki .se7xv{--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .s-Lb2, html code.shiki .s-Lb2{--shiki-default:#85E89D;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":38,"searchDepth":748,"depth":748,"links":6497},[6498,6499,6503,6504,6505,6506,6507,6508,6509,6511,6512],{"id":4976,"depth":276,"text":4977},{"id":5088,"depth":276,"text":5088,"children":6500},[6501,6502],{"id":5091,"depth":748,"text":5092},{"id":5107,"depth":748,"text":5108},{"id":5172,"depth":276,"text":5173},{"id":5336,"depth":276,"text":5337},{"id":5903,"depth":276,"text":5904},{"id":6075,"depth":276,"text":6076},{"id":6172,"depth":276,"text":6172},{"id":6227,"depth":276,"text":6227},{"id":6326,"depth":276,"text":6510},"250 は最終到達ではない",{"id":6360,"depth":276,"text":6360},{"id":6458,"depth":276,"text":6458,"children":6513},[6514],{"id":3185,"depth":748,"text":3185},"Cloudflare Email Service の SMTP を使うための実践ガイド。ドメイン、API Token、cURL、Nodemailer、Python、制限、応答コード、再試行、安全対策を解説します。",{"src":6517,"alt":6518,"caption":6519,"width":6520,"height":6521},"\u002Fmedia\u002Fposts\u002Fcloudflare-smtp-email-sending-guide\u002Fcover-v1.webp","暗号化された SMTP 接続からグローバル配信ネットワークへ入るトランザクションメール","既存アプリを Cloudflare Email Service へ接続するために送信層を作り直す必要はありません。標準 SMTP が互換性のある経路になります",1672,941,{},"\u002Fblog\u002Fcloudflare-smtp-email-sending-guide","2026-07-31",{"title":4934,"description":6515},"blog\u002Fcloudflare-smtp-email-sending-guide",[2654,6528,6529,6530],"SMTP","Email","Nodemailer","REu0oOC10d1Nm7IQ9haszBgo0MBePJrPZLfDTeid4MA",{"id":6533,"title":6534,"accent":1300,"body":6535,"description":8908,"draft":775,"experience":776,"extension":777,"featured":778,"image":8909,"meta":8915,"navigation":778,"path":8916,"publishedAt":8917,"readingTime":2239,"seo":8918,"stem":8919,"tags":8920,"updatedAt":776,"video":8923,"__hash__":8928,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fcloudflare-email-service-send-receive-guide.md","Cloudflare Email Service 送受信実践ガイド—Routing、Workers、API、SMTP まで",{"type":13,"value":6536,"toc":8875},[6537,6544,6547,6558,6578,6585,6588,6593,6597,6600,6651,6654,6660,6663,6669,6675,6680,6684,6687,6691,6694,6697,6717,6721,6724,6727,6741,6744,6748,6751,6755,6758,6769,6772,6780,6787,6804,6807,6818,6821,6827,6830,6834,6837,6853,6856,6863,6872,7204,7213,7306,7321,7325,7332,7462,7465,7482,7486,7512,7515,7536,7539,7543,7546,7553,7621,7627,7630,7649,7655,7659,7862,7868,7872,7875,8192,8195,8201,8206,8210,8213,8263,8266,8269,8289,8292,8295,8417,8420,8423,8426,8429,8435,8438,8461,8472,8475,8478,8481,8487,8490,8496,8500,8503,8527,8530,8550,8553,8557,8567,8573,8576,8579,8597,8600,8659,8662,8665,8697,8700,8703,8723,8726,8743,8746,8749,8752,8755,8758,8779,8782,8784,8872],[16,6538,6539,6540,6543],{},"長い間、Cloudflare とメールの関係は主に二つでした。DNS を管理することと、Email Routing で ",[25,6541,6542],{},"support@example.com"," を実際のメールボックスへ転送することです。",[16,6545,6546],{},"2026 年、この境界は大きく前へ進みました。",[16,6548,6549,6550,6553,6554,6557],{},"Cloudflare は 4 月に ",[20,6551,6552],{},"Email Sending"," を public beta へ移行し、Workers から ",[25,6555,6556],{},"env.EMAIL.send()"," を直接呼んでトランザクションメールを送れるようにしました。6 月には認証 SMTP も追加されました。従来の Email Routing と Email Workers を組み合わせると、Cloudflare Email Service は現在、次の処理を行えます。",[97,6559,6560,6563,6566,6569,6572,6575],{},[100,6561,6562],{},"独自ドメイン宛てのメールを受信する。",[100,6564,6565],{},"検証済みの実メールボックスへ転送する。",[100,6567,6568],{},"受信メールを Worker に渡し、解析、フィルター、保存、自動化を行う。",[100,6570,6571],{},"元のメールスレッド内で返信する。",[100,6573,6574],{},"Worker、REST API、SMTP からトランザクションメールを送る。",[100,6576,6577],{},"送信ログ、bounce、suppression、配信指標を確認する。",[16,6579,6580,6581,6584],{},"この記事は ",[20,6582,6583],{},"2026 年 7 月 28 日","時点の公式機能を基準に、Cloudflare のメール送受信モデル全体を整理し、Nuxt、Workers、D1 プロジェクトに適した実装方法を示します。",[16,6586,6587],{},"先に結論を述べます。",[1355,6589,6590],{},[16,6591,6592],{},"Cloudflare Email Service は従来型の IMAP \u002F POP3 メールボックスでも、Web 画面でメールを読む企業向けスイートでもありません。「メールの入口、送信チャネル、プログラム可能なイベント」の集合として捉えると分かりやすく、認証コード、パスワードリセット、注文通知、システムアラート、サポートチケット、メール駆動の自動化に適しています。",[81,6594,6596],{"id":6595},"最初に受信と送信を分ける","最初に「受信」と「送信」を分ける",[16,6598,6599],{},"メールの送受信は一つの機能に見えますが、Cloudflare では独立した二つの経路です。",[141,6601,6602,6618],{},[144,6603,6604],{},[147,6605,6606,6609,6612,6615],{},[150,6607,6608],{},"方向",[150,6610,6611],{},"Cloudflare 製品",[150,6613,6614],{},"入口",[150,6616,6617],{},"Worker でできること",[160,6619,6620,6634],{},[147,6621,6622,6625,6628,6631],{},[165,6623,6624],{},"受信",[165,6626,6627],{},"Email Routing \u002F Email Workers",[165,6629,6630],{},"外部メールサーバーが Cloudflare MX へ接続",[165,6632,6633],{},"転送、返信、拒否、解析、D1\u002FR2 への保存、Queue への投入",[147,6635,6636,6639,6641,6644],{},[165,6637,6638],{},"送信",[165,6640,6552],{},[165,6642,6643],{},"Workers binding、REST API、認証 SMTP",[165,6645,6646,6647,6650],{},"text\u002FHTML、添付、カスタム header を送り、",[25,6648,6649],{},"messageId"," を取得",[16,6652,6653],{},"受信経路はおおよそ次のとおりです。",[33,6655,6658],{"className":6656,"code":6657,"language":233,"meta":38},[231],"送信元 SMTP\n  → Cloudflare MX\n  → SPF \u002F DKIM \u002F DMARC と reputation の確認\n  → アドレスルールまたは Catch-all に一致\n  → メールボックスへ転送 \u002F Worker \u002F drop\n",[25,6659,6657],{"__ignoreMap":38},[16,6661,6662],{},"送信経路は次のとおりです。",[33,6664,6667],{"className":6665,"code":6666,"language":233,"meta":38},[231],"Nuxt API \u002F Worker \u002F 外部アプリケーション\n  → EMAIL binding \u002F REST API \u002F SMTP\n  → 送信ドメインとパラメータの検証\n  → Cloudflare 配信パイプライン\n  → 受信側メールサーバー\n",[25,6668,6666],{"__ignoreMap":38},[16,6670,6671],{},[1994,6672],{"alt":6673,"src":6674},"メールはエッジ入口で検査され、実メールボックス、Worker 処理ノード、拒否経路へ振り分けられる","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Finbound-routing-v1.webp",[16,6676,6677],{},[2001,6678,6679],{},"これは概念図です。実際の判断には SMTP セッション、メール認証、ルールマッチ、Worker 実行制限も含まれます。",[81,6681,6683],{"id":6682},"_2026-年の製品境界と料金","2026 年の製品境界と料金",[16,6685,6686],{},"Cloudflare は現在、受信と送信を Email Service の下にまとめていますが、利用できるプランは異なります。",[248,6688,6690],{"id":6689},"email-routing受信と転送","Email Routing：受信と転送",[16,6692,6693],{},"Email Routing は Workers Free と Workers Paid の両方で利用できます。公式料金ページでは受信メール数は無制限とされていますが、Worker が処理するメールは対応する Worker リクエストと計算資源を消費します。",[16,6695,6696],{},"適している用途は次のとおりです。",[97,6698,6699,6705,6708,6714],{},[100,6700,6701,6704],{},[25,6702,6703],{},"support@yourdomain.com"," を既存の個人メールボックスへ転送する。",[100,6706,6707],{},"サイトごとに独立した連絡先アドレスを作る。",[100,6709,6710,6713],{},[25,6711,6712],{},"orders+shop-a@yourdomain.com"," のような sub-address で送信元を区別する。",[100,6715,6716],{},"受信メールを Worker でチケット、Webhook、データベースレコードへ変換する。",[248,6718,6720],{"id":6719},"email-sendingトランザクションメール","Email Sending：トランザクションメール",[16,6722,6723],{},"Email Sending は現在も public beta です。",[16,6725,6726],{},"任意の受信者への送信には Workers Paid プランが必要です。2026 年 7 月の公式料金では、",[97,6728,6729,6732,6735,6738],{},[100,6730,6731],{},"アカウントごとに月 3,000 通の送信が含まれる。",[100,6733,6734],{},"超過後は 1,000 通あたり 0.35 ドル。",[100,6736,6737],{},"アカウント内の「検証済み宛先アドレス」へのメールは常に無料で、含まれる枠を消費しない。",[100,6739,6740],{},"Email Service に受理されたメールは、後で hard bounce しても通常は枠を消費する。API 境界で直接拒否されたリクエストは消費しない。",[16,6742,6743],{},"料金と beta 状態は今後も変わる可能性があります。本番導入前に公式料金ページを再確認し、この記事の数値を予算モデルへ固定しないでください。",[81,6745,6747],{"id":6746},"第-1-歩ドメインを正しく接続する","第 1 歩：ドメインを正しく接続する",[16,6749,6750],{},"Email Service は Cloudflare DNS を利用するドメインを必要とします。送信と受信では用途の異なる二組の DNS record が作られます。",[248,6752,6754],{"id":6753},"受信はルートドメインの-record-を使う","受信はルートドメインの record を使う",[16,6756,6757],{},"Email Routing を有効にすると、Cloudflare はドメインルートに次を設定します。",[97,6759,6760,6763,6766],{},[100,6761,6762],{},"MX：受信メールを Cloudflare のメール入口へ届ける。",[100,6764,6765],{},"SPF：Cloudflare による転送を許可する。",[100,6767,6768],{},"DKIM：転送経路へ署名を付与する。",[16,6770,6771],{},"これらは「第三者が自分のドメインへメールを送る」経路を担当します。",[248,6773,6775,6776,6779],{"id":6774},"送信は-cf-bounce-record-を使う","送信は ",[25,6777,6778],{},"cf-bounce"," record を使う",[16,6781,6782,6783,6786],{},"Email Sending を有効にすると、Cloudflare は ",[25,6784,6785],{},"cf-bounce.yourdomain.com"," に次を設定します。",[97,6788,6789,6792,6795,6798],{},[100,6790,6791],{},"MX：bounce を処理する。",[100,6793,6794],{},"SPF：Cloudflare がドメインを代理して送信することを許可する。",[100,6796,6797],{},"DKIM：メール内容が転送中に変更されていないことを検証する。",[100,6799,6800,6803],{},[25,6801,6802],{},"_dmarc.yourdomain.com","：認証失敗時の処理方針を宣言する。",[16,6805,6806],{},"送信と受信は異なる DKIM selector を使います。DKIM record が二つあっても重複設定ではありません。",[16,6808,6809,6810,6813,6814,6817],{},"Google Workspace、Microsoft 365、その他のメールサービスをすでに使っている場合は、MX と SPF の競合を特に確認してください。一つのドメインに有効な SPF policy は一つだけです。複数サービスを同時に許可する場合は ",[25,6811,6812],{},"include"," を統合し、二つ目の ",[25,6815,6816],{},"v=spf1"," を作らないでください。",[16,6819,6820],{},"reputation の用途ごとに subdomain を分けることを勧めます。",[33,6822,6825],{"className":6823,"code":6824,"language":233,"meta":38},[231],"noreply@example.com          アカウント認証とパスワードリセット\nnotify@mail.example.com      プロダクト通知\nsupport@example.com          人によるサポートと受信処理\n",[25,6826,6824],{"__ignoreMap":38},[16,6828,6829],{},"トランザクション通知、人とのやり取り、将来の一括メールが互いの reputation を損なわず、障害と移行の境界も明確になります。",[81,6831,6833],{"id":6832},"メール受信転送ルールから-email-worker-まで","メール受信：転送ルールから Email Worker まで",[16,6835,6836],{},"最も簡単な受信方法にはコードが不要です。",[408,6838,6839,6842,6847,6850],{},[100,6840,6841],{},"Email Routing に検証済みの宛先メールボックスを追加する。",[100,6843,6844,6846],{},[25,6845,6703],{}," を作成する。",[100,6848,6849],{},"「メールへ送信」を選択する。",[100,6851,6852],{},"Cloudflare が受信メールを宛先へ転送する。",[16,6854,6855],{},"件名で分類し、添付を抽出し、チケットを作り、自動返信したい場合は、action を「Worker へ送信」に変えます。",[248,6857,6859,6862],{"id":6858},"email-が受信メールの入口",[25,6860,6861],{},"email()"," が受信メールの入口",[16,6864,6865,6866,6869,6870,5072],{},"Email Worker の入口は ",[25,6867,6868],{},"fetch()"," ではなく ",[25,6871,6861],{},[33,6873,6875],{"className":5396,"code":6874,"language":5398,"meta":38,"style":38},"interface Env {\n  SUPPORT_FORWARD_TO: string\n}\n\nexport default {\n  async email(message, env): Promise\u003Cvoid> {\n    const recipient = message.to.toLowerCase()\n    const subject = message.headers.get('subject') ?? ''\n\n    if (message.rawSize > 10 * 1024 * 1024) {\n      message.setReject('Message is too large for this mailbox')\n      return\n    }\n\n    if (recipient === 'support@example.com') {\n      console.log('Support email accepted', {\n        fromDomain: message.from.split('@').at(-1),\n        subjectLength: subject.length,\n        rawSize: message.rawSize,\n      })\n\n      await message.forward(env.SUPPORT_FORWARD_TO)\n      return\n    }\n\n    message.setReject('Unknown recipient')\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[25,6876,6877,6885,6895,6899,6903,6912,6947,6966,6995,6999,7025,7040,7045,7049,7053,7068,7083,7113,7122,7127,7132,7136,7155,7159,7163,7167,7181,7185],{"__ignoreMap":38},[42,6878,6879,6881,6883],{"class":44,"line":45},[42,6880,5924],{"class":279},[42,6882,5927],{"class":48},[42,6884,5930],{"class":1788},[42,6886,6887,6890,6892],{"class":44,"line":276},[42,6888,6889],{"class":5734},"  SUPPORT_FORWARD_TO",[42,6891,5737],{"class":279},[42,6893,6894],{"class":52}," string\n",[42,6896,6897],{"class":44,"line":748},[42,6898,4116],{"class":1788},[42,6900,6901],{"class":44,"line":1590},[42,6902,1587],{"emptyLinePlaceholder":778},[42,6904,6905,6907,6910],{"class":44,"line":1609},[42,6906,5211],{"class":279},[42,6908,6909],{"class":279}," default",[42,6911,5930],{"class":1788},[42,6913,6914,6917,6920,6922,6925,6927,6930,6933,6935,6938,6941,6944],{"class":44,"line":1624},[42,6915,6916],{"class":279},"  async",[42,6918,6919],{"class":48}," email",[42,6921,5622],{"class":1788},[42,6923,6924],{"class":5734},"message",[42,6926,5743],{"class":1788},[42,6928,6929],{"class":5734},"env",[42,6931,6932],{"class":1788},")",[42,6934,5737],{"class":279},[42,6936,6937],{"class":48}," Promise",[42,6939,6940],{"class":1788},"\u003C",[42,6942,6943],{"class":52},"void",[42,6945,6946],{"class":1788},"> {\n",[42,6948,6949,6952,6955,6957,6960,6963],{"class":44,"line":1633},[42,6950,6951],{"class":279},"    const",[42,6953,6954],{"class":52}," recipient",[42,6956,5429],{"class":279},[42,6958,6959],{"class":1788}," message.to.",[42,6961,6962],{"class":48},"toLowerCase",[42,6964,6965],{"class":1788},"()\n",[42,6967,6968,6970,6973,6975,6978,6981,6983,6986,6989,6992],{"class":44,"line":1646},[42,6969,6951],{"class":279},[42,6971,6972],{"class":52}," subject",[42,6974,5429],{"class":279},[42,6976,6977],{"class":1788}," message.headers.",[42,6979,6980],{"class":48},"get",[42,6982,5622],{"class":1788},[42,6984,6985],{"class":56},"'subject'",[42,6987,6988],{"class":1788},") ",[42,6990,6991],{"class":279},"??",[42,6993,6994],{"class":56}," ''\n",[42,6996,6997],{"class":44,"line":1651},[42,6998,1587],{"emptyLinePlaceholder":778},[42,7000,7001,7004,7007,7010,7013,7016,7019,7021,7023],{"class":44,"line":1674},[42,7002,7003],{"class":279},"    if",[42,7005,7006],{"class":1788}," (message.rawSize ",[42,7008,7009],{"class":279},">",[42,7011,7012],{"class":52}," 10",[42,7014,7015],{"class":279}," *",[42,7017,7018],{"class":52}," 1024",[42,7020,7015],{"class":279},[42,7022,7018],{"class":52},[42,7024,5753],{"class":1788},[42,7026,7027,7030,7033,7035,7038],{"class":44,"line":1680},[42,7028,7029],{"class":1788},"      message.",[42,7031,7032],{"class":48},"setReject",[42,7034,5622],{"class":1788},[42,7036,7037],{"class":56},"'Message is too large for this mailbox'",[42,7039,5791],{"class":1788},[42,7041,7042],{"class":44,"line":1686},[42,7043,7044],{"class":279},"      return\n",[42,7046,7047],{"class":44,"line":788},[42,7048,4106],{"class":1788},[42,7050,7051],{"class":44,"line":1697},[42,7052,1587],{"emptyLinePlaceholder":778},[42,7054,7055,7057,7060,7063,7066],{"class":44,"line":1703},[42,7056,7003],{"class":279},[42,7058,7059],{"class":1788}," (recipient ",[42,7061,7062],{"class":279},"===",[42,7064,7065],{"class":56}," 'support@example.com'",[42,7067,5753],{"class":1788},[42,7069,7070,7073,7076,7078,7081],{"class":44,"line":1709},[42,7071,7072],{"class":1788},"      console.",[42,7074,7075],{"class":48},"log",[42,7077,5622],{"class":1788},[42,7079,7080],{"class":56},"'Support email accepted'",[42,7082,5628],{"class":1788},[42,7084,7085,7088,7091,7093,7096,7099,7102,7104,7107,7110],{"class":44,"line":1715},[42,7086,7087],{"class":1788},"        fromDomain: message.from.",[42,7089,7090],{"class":48},"split",[42,7092,5622],{"class":1788},[42,7094,7095],{"class":56},"'@'",[42,7097,7098],{"class":1788},").",[42,7100,7101],{"class":48},"at",[42,7103,5622],{"class":1788},[42,7105,7106],{"class":279},"-",[42,7108,7109],{"class":52},"1",[42,7111,7112],{"class":1788},"),\n",[42,7114,7115,7118,7120],{"class":44,"line":1720},[42,7116,7117],{"class":1788},"        subjectLength: subject.",[42,7119,5641],{"class":52},[42,7121,3988],{"class":1788},[42,7123,7124],{"class":44,"line":1729},[42,7125,7126],{"class":1788},"        rawSize: message.rawSize,\n",[42,7128,7129],{"class":44,"line":1742},[42,7130,7131],{"class":1788},"      })\n",[42,7133,7134],{"class":44,"line":1756},[42,7135,1587],{"emptyLinePlaceholder":778},[42,7137,7138,7141,7144,7147,7150,7153],{"class":44,"line":2239},[42,7139,7140],{"class":279},"      await",[42,7142,7143],{"class":1788}," message.",[42,7145,7146],{"class":48},"forward",[42,7148,7149],{"class":1788},"(env.",[42,7151,7152],{"class":52},"SUPPORT_FORWARD_TO",[42,7154,5791],{"class":1788},[42,7156,7157],{"class":44,"line":2249},[42,7158,7044],{"class":279},[42,7160,7161],{"class":44,"line":2259},[42,7162,4106],{"class":1788},[42,7164,7165],{"class":44,"line":2269},[42,7166,1587],{"emptyLinePlaceholder":778},[42,7168,7169,7172,7174,7176,7179],{"class":44,"line":2274},[42,7170,7171],{"class":1788},"    message.",[42,7173,7032],{"class":48},[42,7175,5622],{"class":1788},[42,7177,7178],{"class":56},"'Unknown recipient'",[42,7180,5791],{"class":1788},[42,7182,7183],{"class":44,"line":2282},[42,7184,5495],{"class":1788},[42,7186,7187,7190,7193,7196,7198,7201],{"class":44,"line":5655},[42,7188,7189],{"class":1788},"} ",[42,7191,7192],{"class":279},"satisfies",[42,7194,7195],{"class":48}," ExportedHandler",[42,7197,6940],{"class":1788},[42,7199,7200],{"class":48},"Env",[42,7202,7203],{"class":1788},">\n",[16,7205,7206,7207,23,7209,7212],{},"ここで ",[25,7208,6924],{},[25,7210,7211],{},"ForwardableEmailMessage"," です。よく使う field と操作は次のとおりです。",[141,7214,7215,7224],{},[144,7216,7217],{},[147,7218,7219,7222],{},[150,7220,7221],{},"機能",[150,7223,2573],{},[160,7225,7226,7236,7246,7256,7266,7276,7286,7296],{},[147,7227,7228,7233],{},[165,7229,7230],{},[25,7231,7232],{},"message.from",[165,7234,7235],{},"SMTP envelope の送信者",[147,7237,7238,7243],{},[165,7239,7240],{},[25,7241,7242],{},"message.to",[165,7244,7245],{},"SMTP envelope の受信者",[147,7247,7248,7253],{},[165,7249,7250],{},[25,7251,7252],{},"message.headers",[165,7254,7255],{},"Subject、Message-ID などの header を読む",[147,7257,7258,7263],{},[165,7259,7260],{},[25,7261,7262],{},"message.raw",[165,7264,7265],{},"生の MIME stream",[147,7267,7268,7273],{},[165,7269,7270],{},[25,7271,7272],{},"message.rawSize",[165,7274,7275],{},"生メールのサイズ",[147,7277,7278,7283],{},[165,7279,7280],{},[25,7281,7282],{},"message.forward(address)",[165,7284,7285],{},"検証済み宛先へ転送する",[147,7287,7288,7293],{},[165,7289,7290],{},[25,7291,7292],{},"message.reply(email)",[165,7294,7295],{},"制約の範囲内で元の送信者へ返信する",[147,7297,7298,7303],{},[165,7299,7300],{},[25,7301,7302],{},"message.setReject(reason)",[165,7304,7305],{},"永続的な SMTP rejection を返す",[16,7307,7308,7309,5329,7311,7313,7314,5329,7317,7320],{},"見落としやすいセキュリティ上の注意があります。",[25,7310,7232],{},[25,7312,7242],{}," は SMTP envelope の値で、メール本文 header 内の ",[25,7315,7316],{},"From:",[25,7318,7319],{},"To:"," とは異なる場合があります。認可、routing、audit では、ユーザーに表示する header アドレスを信頼できる identity と誤認しないでください。",[248,7322,7324],{"id":7323},"本文が必要なときだけ-mime-を解析する","本文が必要なときだけ MIME を解析する",[16,7326,7327,7328,7331],{},"転送またはアドレスによる routing だけなら、メール全体を解析する必要はありません。本文と添付を取り出す場合は ",[25,7329,7330],{},"postal-mime"," を使えます。",[33,7333,7335],{"className":5396,"code":7334,"language":5398,"meta":38,"style":38},"import PostalMime from 'postal-mime'\n\nexport default {\n  async email(message): Promise\u003Cvoid> {\n    const parsed = await PostalMime.parse(message.raw)\n\n    console.log({\n      subject: parsed.subject,\n      textLength: parsed.text?.length ?? 0,\n      attachmentCount: parsed.attachments.length,\n    })\n  },\n} satisfies ExportedHandler\n",[25,7336,7337,7349,7353,7361,7383,7403,7407,7416,7421,7435,7444,7449,7453],{"__ignoreMap":38},[42,7338,7339,7341,7344,7346],{"class":44,"line":45},[42,7340,5405],{"class":279},[42,7342,7343],{"class":1788}," PostalMime ",[42,7345,5411],{"class":279},[42,7347,7348],{"class":56}," 'postal-mime'\n",[42,7350,7351],{"class":44,"line":276},[42,7352,1587],{"emptyLinePlaceholder":778},[42,7354,7355,7357,7359],{"class":44,"line":748},[42,7356,5211],{"class":279},[42,7358,6909],{"class":279},[42,7360,5930],{"class":1788},[42,7362,7363,7365,7367,7369,7371,7373,7375,7377,7379,7381],{"class":44,"line":1590},[42,7364,6916],{"class":279},[42,7366,6919],{"class":48},[42,7368,5622],{"class":1788},[42,7370,6924],{"class":5734},[42,7372,6932],{"class":1788},[42,7374,5737],{"class":279},[42,7376,6937],{"class":48},[42,7378,6940],{"class":1788},[42,7380,6943],{"class":52},[42,7382,6946],{"class":1788},[42,7384,7385,7387,7390,7392,7394,7397,7400],{"class":44,"line":1609},[42,7386,6951],{"class":279},[42,7388,7389],{"class":52}," parsed",[42,7391,5429],{"class":279},[42,7393,5545],{"class":279},[42,7395,7396],{"class":1788}," PostalMime.",[42,7398,7399],{"class":48},"parse",[42,7401,7402],{"class":1788},"(message.raw)\n",[42,7404,7405],{"class":44,"line":1624},[42,7406,1587],{"emptyLinePlaceholder":778},[42,7408,7409,7412,7414],{"class":44,"line":1633},[42,7410,7411],{"class":1788},"    console.",[42,7413,7075],{"class":48},[42,7415,5438],{"class":1788},[42,7417,7418],{"class":44,"line":1646},[42,7419,7420],{"class":1788},"      subject: parsed.subject,\n",[42,7422,7423,7426,7428,7431,7433],{"class":44,"line":1651},[42,7424,7425],{"class":1788},"      textLength: parsed.text?.",[42,7427,5641],{"class":52},[42,7429,7430],{"class":279}," ??",[42,7432,2861],{"class":52},[42,7434,3988],{"class":1788},[42,7436,7437,7440,7442],{"class":44,"line":1674},[42,7438,7439],{"class":1788},"      attachmentCount: parsed.attachments.",[42,7441,5641],{"class":52},[42,7443,3988],{"class":1788},[42,7445,7446],{"class":44,"line":1680},[42,7447,7448],{"class":1788},"    })\n",[42,7450,7451],{"class":44,"line":1686},[42,7452,5495],{"class":1788},[42,7454,7455,7457,7459],{"class":44,"line":788},[42,7456,7189],{"class":1788},[42,7458,7192],{"class":279},[42,7460,7461],{"class":48}," ExportedHandler\n",[16,7463,7464],{},"解析後の HTML、ファイル名、Content-Type、添付はすべて外部入力です。安全な方法は次のとおりです。",[97,7466,7467,7470,7473,7476,7479],{},[100,7468,7469],{},"管理画面へ受信 HTML を直接挿入しない。",[100,7471,7472],{},"本文、添付数、展開後サイズを制限する。",[100,7474,7475],{},"R2 へ保存する前に新しい object key を生成し、元のファイル名を path に使わない。",[100,7477,7478],{},"メール本文から SQL、Shell、template、AI ツールパラメータを直接組み立てない。",[100,7480,7481],{},"virus scan、OCR、モデル呼び出しを受信イベントの同期 CPU 時間へ詰め込まず、重い処理は Queue へ渡す。",[248,7483,7485],{"id":7484},"転送返信拒否silent-drop-の違い","転送、返信、拒否、silent drop の違い",[97,7487,7488,7494,7500,7506],{},[100,7489,7490,7493],{},[20,7491,7492],{},"転送","：人のメールボックスで処理を続ける用途。宛先は事前に検証が必要。",[100,7495,7496,7499],{},[20,7497,7498],{},"返信","：自動確認やメール bot に適するが、追加制約がある。",[100,7501,7502,7505],{},[20,7503,7504],{},"拒否","：SMTP 段階で送信元へ受け付けなかったことを伝える。",[100,7507,7508,7511],{},[20,7509,7510],{},"drop","：ルール一致後に処理を止め、送信元へ業務レベルの応答を返さない。",[16,7513,7514],{},"自動返信を任意の送信 interface として使うことはできません。現在の公式要件は次のとおりです。",[97,7516,7517,7520,7523,7526,7529],{},[100,7518,7519],{},"受信メールに有効な DMARC 結果がある。",[100,7521,7522],{},"一つの受信イベントにつき返信は一度だけ。",[100,7524,7525],{},"返信先は元の送信者である。",[100,7527,7528],{},"返信の送信ドメインは受信したドメインと同じである。",[100,7530,7531,7532,7535],{},"メールループ防止のため、",[25,7533,7534],{},"References"," が 100 項目を超えると返信を拒否する。",[16,7537,7538],{},"この制約はむしろ有益です。「このメールへ返信する」と「任意のアドレスへ新規メールを送る」が異なる権限になります。",[81,7540,7542],{"id":7541},"メール送信workers-binding-を優先する","メール送信：Workers binding を優先する",[16,7544,7545],{},"アプリケーションが Cloudflare Workers 上で動いているなら、Workers binding が最も自然です。業務コードに Cloudflare API Token を保存する必要がありません。",[16,7547,7548,7549,7552],{},"まず ",[25,7550,7551],{},"wrangler.jsonc"," で binding を宣言します。",[33,7554,7558],{"className":7555,"code":7556,"language":7557,"meta":38,"style":38},"language-jsonc shiki shiki-themes github-dark github-dark","{\n  \"send_email\": [\n    {\n      \"name\": \"EMAIL\",\n      \"allowed_sender_addresses\": [\n        \"noreply@example.com\"\n      ]\n    }\n  ],\n  \"vars\": {\n    \"EMAIL_FROM\": \"noreply@example.com\"\n  }\n}\n","jsonc",[25,7559,7560,7564,7569,7574,7579,7584,7589,7594,7598,7603,7608,7613,7617],{"__ignoreMap":38},[42,7561,7562],{"class":44,"line":45},[42,7563,3975],{},[42,7565,7566],{"class":44,"line":276},[42,7567,7568],{},"  \"send_email\": [\n",[42,7570,7571],{"class":44,"line":748},[42,7572,7573],{},"    {\n",[42,7575,7576],{"class":44,"line":1590},[42,7577,7578],{},"      \"name\": \"EMAIL\",\n",[42,7580,7581],{"class":44,"line":1609},[42,7582,7583],{},"      \"allowed_sender_addresses\": [\n",[42,7585,7586],{"class":44,"line":1624},[42,7587,7588],{},"        \"noreply@example.com\"\n",[42,7590,7591],{"class":44,"line":1633},[42,7592,7593],{},"      ]\n",[42,7595,7596],{"class":44,"line":1646},[42,7597,4106],{},[42,7599,7600],{"class":44,"line":1651},[42,7601,7602],{},"  ],\n",[42,7604,7605],{"class":44,"line":1674},[42,7606,7607],{},"  \"vars\": {\n",[42,7609,7610],{"class":44,"line":1680},[42,7611,7612],{},"    \"EMAIL_FROM\": \"noreply@example.com\"\n",[42,7614,7615],{"class":44,"line":1686},[42,7616,4111],{},[42,7618,7619],{"class":44,"line":788},[42,7620,4116],{},[16,7622,7623,7626],{},[25,7624,7625],{},"allowed_sender_addresses"," は説明用の設定ではなく、実行時の権限境界です。endpoint が誤用されても、この binding は別の送信者アドレスになりすませません。",[16,7628,7629],{},"次の設定で宛先も制限できます。",[97,7631,7632,7638,7644],{},[100,7633,7634,7637],{},[25,7635,7636],{},"destination_address","：宛先を一つに固定する。",[100,7639,7640,7643],{},[25,7641,7642],{},"allowed_destination_addresses","：宛先を許可リストに限定する。",[100,7645,7646,7648],{},[25,7647,7625],{},"：利用できる送信元アドレスを制限する。",[16,7650,7651,7652,7654],{},"システムアラート用 Worker は ",[25,7653,7636],{}," の固定に適しています。ユーザー認証サービスでは通常、送信者を制限し、アプリ側で宛先と頻度を検証します。",[248,7656,7658],{"id":7657},"worker-内から送信する","Worker 内から送信する",[33,7660,7662],{"className":5396,"code":7661,"language":5398,"meta":38,"style":38},"interface Env {\n  EMAIL: SendEmail\n}\n\nexport default {\n  async fetch(_request, env): Promise\u003CResponse> {\n    const result = await env.EMAIL.send({\n      from: {\n        email: 'noreply@example.com',\n        name: 'Example App',\n      },\n      to: 'reader@example.net',\n      subject: 'メールアドレスを確認してください',\n      text: '認証コードは 482913 です。5 分間有効です。',\n      html: '\u003Cp>認証コードは \u003Cstrong>482913\u003C\u002Fstrong> です。5 分間有効です。\u003C\u002Fp>',\n    })\n\n    return Response.json({\n      messageId: result.messageId,\n    })\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[25,7663,7664,7672,7680,7684,7688,7696,7725,7746,7751,7761,7771,7775,7785,7795,7805,7815,7819,7823,7835,7840,7844,7848],{"__ignoreMap":38},[42,7665,7666,7668,7670],{"class":44,"line":45},[42,7667,5924],{"class":279},[42,7669,5927],{"class":48},[42,7671,5930],{"class":1788},[42,7673,7674,7676,7678],{"class":44,"line":276},[42,7675,5935],{"class":5734},[42,7677,5737],{"class":279},[42,7679,5940],{"class":48},[42,7681,7682],{"class":44,"line":748},[42,7683,4116],{"class":1788},[42,7685,7686],{"class":44,"line":1590},[42,7687,1587],{"emptyLinePlaceholder":778},[42,7689,7690,7692,7694],{"class":44,"line":1609},[42,7691,5211],{"class":279},[42,7693,6909],{"class":279},[42,7695,5930],{"class":1788},[42,7697,7698,7700,7703,7705,7708,7710,7712,7714,7716,7718,7720,7723],{"class":44,"line":1624},[42,7699,6916],{"class":279},[42,7701,7702],{"class":48}," fetch",[42,7704,5622],{"class":1788},[42,7706,7707],{"class":5734},"_request",[42,7709,5743],{"class":1788},[42,7711,6929],{"class":5734},[42,7713,6932],{"class":1788},[42,7715,5737],{"class":279},[42,7717,6937],{"class":48},[42,7719,6940],{"class":1788},[42,7721,7722],{"class":48},"Response",[42,7724,6946],{"class":1788},[42,7726,7727,7729,7732,7734,7736,7738,7740,7742,7744],{"class":44,"line":1633},[42,7728,6951],{"class":279},[42,7730,7731],{"class":52}," result",[42,7733,5429],{"class":279},[42,7735,5545],{"class":279},[42,7737,5956],{"class":1788},[42,7739,5959],{"class":52},[42,7741,1782],{"class":1788},[42,7743,5964],{"class":48},[42,7745,5438],{"class":1788},[42,7747,7748],{"class":44,"line":1646},[42,7749,7750],{"class":1788},"      from: {\n",[42,7752,7753,7756,7759],{"class":44,"line":1651},[42,7754,7755],{"class":1788},"        email: ",[42,7757,7758],{"class":56},"'noreply@example.com'",[42,7760,3988],{"class":1788},[42,7762,7763,7766,7769],{"class":44,"line":1674},[42,7764,7765],{"class":1788},"        name: ",[42,7767,7768],{"class":56},"'Example App'",[42,7770,3988],{"class":1788},[42,7772,7773],{"class":44,"line":1680},[42,7774,4092],{"class":1788},[42,7776,7777,7780,7783],{"class":44,"line":1686},[42,7778,7779],{"class":1788},"      to: ",[42,7781,7782],{"class":56},"'reader@example.net'",[42,7784,3988],{"class":1788},[42,7786,7787,7790,7793],{"class":44,"line":788},[42,7788,7789],{"class":1788},"      subject: ",[42,7791,7792],{"class":56},"'メールアドレスを確認してください'",[42,7794,3988],{"class":1788},[42,7796,7797,7800,7803],{"class":44,"line":1697},[42,7798,7799],{"class":1788},"      text: ",[42,7801,7802],{"class":56},"'認証コードは 482913 です。5 分間有効です。'",[42,7804,3988],{"class":1788},[42,7806,7807,7810,7813],{"class":44,"line":1703},[42,7808,7809],{"class":1788},"      html: ",[42,7811,7812],{"class":56},"'\u003Cp>認証コードは \u003Cstrong>482913\u003C\u002Fstrong> です。5 分間有効です。\u003C\u002Fp>'",[42,7814,3988],{"class":1788},[42,7816,7817],{"class":44,"line":1709},[42,7818,7448],{"class":1788},[42,7820,7821],{"class":44,"line":1715},[42,7822,1587],{"emptyLinePlaceholder":778},[42,7824,7825,7828,7831,7833],{"class":44,"line":1720},[42,7826,7827],{"class":279},"    return",[42,7829,7830],{"class":1788}," Response.",[42,7832,3968],{"class":48},[42,7834,5438],{"class":1788},[42,7836,7837],{"class":44,"line":1729},[42,7838,7839],{"class":1788},"      messageId: result.messageId,\n",[42,7841,7842],{"class":44,"line":1742},[42,7843,7448],{"class":1788},[42,7845,7846],{"class":44,"line":1756},[42,7847,5495],{"class":1788},[42,7849,7850,7852,7854,7856,7858,7860],{"class":44,"line":2239},[42,7851,7189],{"class":1788},[42,7853,7192],{"class":279},[42,7855,7195],{"class":48},[42,7857,6940],{"class":1788},[42,7859,7200],{"class":48},[42,7861,7203],{"class":1788},[16,7863,7864,7865,7867],{},"Workers binding が成功すると ",[25,7866,6649],{}," を返します。これは Cloudflare が送信リクエストを受理したことを意味し、ユーザーの受信トレイにすでに表示されたことを保証しません。最終配信、遅延、bounce、suppression は Email Service の log と metric で確認します。",[248,7869,7871],{"id":7870},"nuxt-のサーバー-api-から送信する","Nuxt のサーバー API から送信する",[16,7873,7874],{},"Nuxt を Workers へデプロイすると、サーバールートから同じ binding を読めます。",[33,7876,7878],{"className":5396,"code":7877,"language":5398,"meta":38,"style":38},"import { z } from 'zod'\n\nconst schema = z.object({\n  email: z.string().trim().email().max(254),\n})\n\nexport default defineEventHandler(async (event) => {\n  const { email } = schema.parse(await readBody(event))\n  const env = event.context.cloudflare.env as {\n    EMAIL: SendEmail\n    EMAIL_FROM: string\n  }\n\n  \u002F\u002F 実際のプロジェクトでは same-origin を確認し、\n  \u002F\u002F IP とメールアドレスを rate-limit し、D1 で\n  \u002F\u002F 一度の送信資格を atomically に取得します。\n  const result = await env.EMAIL.send({\n    from: {\n      email: env.EMAIL_FROM,\n      name: 'Example App',\n    },\n    to: email,\n    subject: 'メールアドレスの確認',\n    text: '認証コードは 5 分後に失効します。',\n    html: '\u003Cp>認証コードは \u003Cstrong>5 分後\u003C\u002Fstrong>に失効します。\u003C\u002Fp>',\n  })\n\n  return {\n    ok: true,\n    messageId: result.messageId,\n  }\n})\n",[25,7879,7880,7892,7896,7913,7944,7948,7952,7978,8008,8024,8033,8042,8046,8050,8056,8061,8066,8086,8091,8101,8110,8115,8120,8130,8140,8150,8155,8159,8166,8176,8182,8187],{"__ignoreMap":38},[42,7881,7882,7884,7887,7889],{"class":44,"line":45},[42,7883,5405],{"class":279},[42,7885,7886],{"class":1788}," { z } ",[42,7888,5411],{"class":279},[42,7890,7891],{"class":56}," 'zod'\n",[42,7893,7894],{"class":44,"line":276},[42,7895,1587],{"emptyLinePlaceholder":778},[42,7897,7898,7900,7903,7905,7908,7911],{"class":44,"line":748},[42,7899,5423],{"class":279},[42,7901,7902],{"class":52}," schema",[42,7904,5429],{"class":279},[42,7906,7907],{"class":1788}," z.",[42,7909,7910],{"class":48},"object",[42,7912,5438],{"class":1788},[42,7914,7915,7918,7921,7924,7927,7929,7932,7934,7937,7939,7942],{"class":44,"line":1590},[42,7916,7917],{"class":1788},"  email: z.",[42,7919,7920],{"class":48},"string",[42,7922,7923],{"class":1788},"().",[42,7925,7926],{"class":48},"trim",[42,7928,7923],{"class":1788},[42,7930,7931],{"class":48},"email",[42,7933,7923],{"class":1788},[42,7935,7936],{"class":48},"max",[42,7938,5622],{"class":1788},[42,7940,7941],{"class":52},"254",[42,7943,7112],{"class":1788},[42,7945,7946],{"class":44,"line":1609},[42,7947,5529],{"class":1788},[42,7949,7950],{"class":44,"line":1624},[42,7951,1587],{"emptyLinePlaceholder":778},[42,7953,7954,7956,7958,7961,7963,7966,7968,7971,7973,7976],{"class":44,"line":1633},[42,7955,5211],{"class":279},[42,7957,6909],{"class":279},[42,7959,7960],{"class":48}," defineEventHandler",[42,7962,5622],{"class":1788},[42,7964,7965],{"class":279},"async",[42,7967,5761],{"class":1788},[42,7969,7970],{"class":5734},"event",[42,7972,6988],{"class":1788},[42,7974,7975],{"class":279},"=>",[42,7977,5930],{"class":1788},[42,7979,7980,7983,7986,7988,7991,7993,7996,7998,8000,8002,8005],{"class":44,"line":1646},[42,7981,7982],{"class":279},"  const",[42,7984,7985],{"class":1788}," { ",[42,7987,7931],{"class":52},[42,7989,7990],{"class":1788}," } ",[42,7992,1832],{"class":279},[42,7994,7995],{"class":1788}," schema.",[42,7997,7399],{"class":48},[42,7999,5622],{"class":1788},[42,8001,5953],{"class":279},[42,8003,8004],{"class":48}," readBody",[42,8006,8007],{"class":1788},"(event))\n",[42,8009,8010,8012,8014,8016,8019,8022],{"class":44,"line":1651},[42,8011,7982],{"class":279},[42,8013,1884],{"class":52},[42,8015,5429],{"class":279},[42,8017,8018],{"class":1788}," event.context.cloudflare.env ",[42,8020,8021],{"class":279},"as",[42,8023,5930],{"class":1788},[42,8025,8026,8029,8031],{"class":44,"line":1674},[42,8027,8028],{"class":5734},"    EMAIL",[42,8030,5737],{"class":279},[42,8032,5940],{"class":48},[42,8034,8035,8038,8040],{"class":44,"line":1680},[42,8036,8037],{"class":5734},"    EMAIL_FROM",[42,8039,5737],{"class":279},[42,8041,6894],{"class":52},[42,8043,8044],{"class":44,"line":1686},[42,8045,4111],{"class":1788},[42,8047,8048],{"class":44,"line":788},[42,8049,1587],{"emptyLinePlaceholder":778},[42,8051,8052],{"class":44,"line":1697},[42,8053,8055],{"class":8054},"sJ8bj","  \u002F\u002F 実際のプロジェクトでは same-origin を確認し、\n",[42,8057,8058],{"class":44,"line":1703},[42,8059,8060],{"class":8054},"  \u002F\u002F IP とメールアドレスを rate-limit し、D1 で\n",[42,8062,8063],{"class":44,"line":1709},[42,8064,8065],{"class":8054},"  \u002F\u002F 一度の送信資格を atomically に取得します。\n",[42,8067,8068,8070,8072,8074,8076,8078,8080,8082,8084],{"class":44,"line":1715},[42,8069,7982],{"class":279},[42,8071,7731],{"class":52},[42,8073,5429],{"class":279},[42,8075,5545],{"class":279},[42,8077,5956],{"class":1788},[42,8079,5959],{"class":52},[42,8081,1782],{"class":1788},[42,8083,5964],{"class":48},[42,8085,5438],{"class":1788},[42,8087,8088],{"class":44,"line":1720},[42,8089,8090],{"class":1788},"    from: {\n",[42,8092,8093,8096,8099],{"class":44,"line":1729},[42,8094,8095],{"class":1788},"      email: env.",[42,8097,8098],{"class":52},"EMAIL_FROM",[42,8100,3988],{"class":1788},[42,8102,8103,8106,8108],{"class":44,"line":1742},[42,8104,8105],{"class":1788},"      name: ",[42,8107,7768],{"class":56},[42,8109,3988],{"class":1788},[42,8111,8112],{"class":44,"line":1756},[42,8113,8114],{"class":1788},"    },\n",[42,8116,8117],{"class":44,"line":2239},[42,8118,8119],{"class":1788},"    to: email,\n",[42,8121,8122,8125,8128],{"class":44,"line":2249},[42,8123,8124],{"class":1788},"    subject: ",[42,8126,8127],{"class":56},"'メールアドレスの確認'",[42,8129,3988],{"class":1788},[42,8131,8132,8135,8138],{"class":44,"line":2259},[42,8133,8134],{"class":1788},"    text: ",[42,8136,8137],{"class":56},"'認証コードは 5 分後に失効します。'",[42,8139,3988],{"class":1788},[42,8141,8142,8145,8148],{"class":44,"line":2269},[42,8143,8144],{"class":1788},"    html: ",[42,8146,8147],{"class":56},"'\u003Cp>認証コードは \u003Cstrong>5 分後\u003C\u002Fstrong>に失効します。\u003C\u002Fp>'",[42,8149,3988],{"class":1788},[42,8151,8152],{"class":44,"line":2274},[42,8153,8154],{"class":1788},"  })\n",[42,8156,8157],{"class":44,"line":2282},[42,8158,1587],{"emptyLinePlaceholder":778},[42,8160,8161,8164],{"class":44,"line":5655},[42,8162,8163],{"class":279},"  return",[42,8165,5930],{"class":1788},[42,8167,8169,8172,8174],{"class":44,"line":8168},29,[42,8170,8171],{"class":1788},"    ok: ",[42,8173,4010],{"class":52},[42,8175,3988],{"class":1788},[42,8177,8179],{"class":44,"line":8178},30,[42,8180,8181],{"class":1788},"    messageId: result.messageId,\n",[42,8183,8185],{"class":44,"line":8184},31,[42,8186,4111],{"class":1788},[42,8188,8190],{"class":44,"line":8189},32,[42,8191,5529],{"class":1788},[16,8193,8194],{},"この例を、匿名で無制限に呼べ、件名と本文を自由に指定できる公開 API にしないでください。自分のドメインが spam relay になります。",[16,8196,8197],{},[1994,8198],{"alt":8199,"src":8200},"Worker が生成したトランザクションメールが複数の認証レイヤーを通り、異なる受信システムへ配信される","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Foutbound-delivery-v1.webp",[16,8202,8203],{},[2001,8204,8205],{},"SPF、DKIM、DMARC は「誰が送信できるか、内容が変更されていないか、失敗時にどう処理するか」を扱います。すべてのメールがメイン受信トレイへ入ることは保証しません。",[81,8207,8209],{"id":8208},"rest-api-と認証-smtp-が適する場面","REST API と認証 SMTP が適する場面",[16,8211,8212],{},"Cloudflare は現在、三つの送信入口を提供しています。",[141,8214,8215,8227],{},[144,8216,8217],{},[147,8218,8219,8221,8224],{},[150,8220,3389],{},[150,8222,8223],{},"最適な用途",[150,8225,8226],{},"認証情報と境界",[160,8228,8229,8240,8250],{},[147,8230,8231,8234,8237],{},[165,8232,8233],{},"Workers binding",[165,8235,8236],{},"すでに Workers 上で動く Nuxt、Hono、Agent",[165,8238,8239],{},"コードに API Token を保存せず、binding で送信者と宛先を制限できる",[147,8241,8242,8244,8247],{},[165,8243,6045],{},[165,8245,8246],{},"別の cloud、CI、backend platform で動くプログラム",[165,8248,8249],{},"最小権限 API Token を使い、response は受信者ごとに delivered、queued、permanent bounce を区別する",[147,8251,8252,8255,8258],{},[165,8253,8254],{},"認証 SMTP",[165,8256,8257],{},"Nodemailer、PHPMailer、JavaMail などの SMTP client を使う既存システム",[165,8259,8260,8262],{},[25,8261,4953],{},"、implicit TLS のみ。password は Email Sending 権限を持つ API Token",[16,8264,8265],{},"認証 SMTP は 2026 年 6 月に追加された beta 機能です。現在は 465 port の SMTPS だけをサポートし、587 port の STARTTLS や匿名利用できる 25 port relay はありません。",[16,8267,8268],{},"SMTP Token が漏れると、所有者はアカウントへ接続された送信ドメインを使ってメールを送れます。そのため、",[97,8270,8271,8274,8280,8283,8286],{},[100,8272,8273],{},"account-owned token を優先する。",[100,8275,8276,8277,8279],{},"権限は ",[25,8278,5038],{}," だけにする。",[100,8281,8282],{},"repository、image、frontend environment variable に書かない。",[100,8284,8285],{},"システムごとに別 Token を使い、個別に revoke できるようにする。",[100,8287,8288],{},"Worker binding を使えるなら、「SMTP に慣れている」という理由だけで長期 credential を追加しない。",[81,8290,8291],{"id":8291},"必ず把握すべき制限",[16,8293,8294],{},"2026 年 7 月時点で、アプリ側のコードで事前に防御すべき主な platform limit は次のとおりです。",[141,8296,8297,8311],{},[144,8298,8299],{},[147,8300,8301,8304,8308],{},[150,8302,8303],{},"制限",[150,8305,8307],{"align":8306},"right","現在値",[150,8309,8310],{},"実装への影響",[160,8312,8313,8324,8334,8345,8356,8367,8377,8388,8399],{},[147,8314,8315,8318,8321],{},[165,8316,8317],{},"ドメインごとの Email Routing ルール",[165,8319,8320],{"align":8306},"200",[165,8322,8323],{},"multi-tenant システムでユーザーごとの静的ルールを作らない",[147,8325,8326,8329,8331],{},[165,8327,8328],{},"アカウントごとの検証済み宛先",[165,8330,8320],{"align":8306},[165,8332,8333],{},"多数のメールボックスへ転送する前に account-level の宛先を設計",[147,8335,8336,8339,8342],{},[165,8337,8338],{},"受信メールサイズ",[165,8340,8341],{"align":8306},"25 MiB",[165,8343,8344],{},"それより大きいメールは SMTP 段階で拒否",[147,8346,8347,8350,8353],{},[165,8348,8349],{},"送信メール 1 通あたりの受信者",[165,8351,8352],{"align":8306},"50",[165,8354,8355],{},"To、Cc、Bcc の合計",[147,8357,8358,8361,8364],{},[165,8359,8360],{},"通常の送信メール総サイズ",[165,8362,8363],{"align":8306},"5 MiB",[165,8365,8366],{},"添付と MIME encoding overhead を含む",[147,8368,8369,8372,8374],{},[165,8370,8371],{},"検証済み宛先への総サイズ",[165,8373,8341],{"align":8306},[165,8375,8376],{},"検証済み宛先アドレスにのみ適用",[147,8378,8379,8382,8385],{},[165,8380,8381],{},"カスタム header の総サイズ",[165,8383,8384],{"align":8306},"16 KB",[165,8386,8387],{},"業務データを Header へ詰め込まない",[147,8389,8390,8393,8396],{},[165,8391,8392],{},"1 Zone の送信\u002Frouting ドメイン数",[165,8394,8395],{"align":8306},"30",[165,8397,8398],{},"root domain と subdomain の両方を数える",[147,8400,8401,8407,8410],{},[165,8402,8403,8404,8406],{},"返信の ",[25,8405,7534],{}," 項目数",[165,8408,8409],{"align":8306},"100",[165,8411,8412,8413,8416],{},"超えると ",[25,8414,8415],{},"reply()"," が失敗",[16,8418,8419],{},"添付は Base64 と MIME で包装されます。3 MiB の生ファイルでも最終メールが 3 MiB のままとは限りません。encoding、本文、header の余裕を残してください。",[16,8421,8422],{},"platform は account reputation、配信成績、利用履歴に応じて一日の送信枠を調整することもあります。「月間枠が残っている」ことを、ある一秒間に無制限の並行送信ができるという意味にしないでください。",[81,8424,8425],{"id":8425},"認証コードとパスワードリセットの設計",[16,8427,8428],{},"このブログの登録・パスワードリセットメールは、次の経路を使います。",[33,8430,8433],{"className":8431,"code":8432,"language":233,"meta":38},[231],"ユーザーがメールアドレスを送信\n  → same-origin check\n  → IP とメールアドレスの rate limit\n  → 6 桁コードを生成\n  → D1 は salted hash、用途、有効期限、試行回数だけを保存\n  → EMAIL binding で送信\n  → 認証成功時に atomically に使用済みへ更新\n",[25,8434,8432],{"__ignoreMap":38},[16,8436,8437],{},"重要な規則は次のとおりです。",[97,8439,8440,8443,8446,8449,8452,8455,8458],{},[100,8441,8442],{},"同じメールアドレスには 5 分に 1 通だけ送る。",[100,8444,8445],{},"認証コードは 5 分後に失効する。",[100,8447,8448],{},"コードは一度だけ使用できる。",[100,8450,8451],{},"D1 に plaintext のコードを保存しない。",[100,8453,8454],{},"連続して間違え、しきい値へ達したら認証を止める。",[100,8456,8457],{},"送信失敗時は取得した枠を解放し、届かなかったコードでユーザーを lock しない。",[100,8459,8460],{},"パスワードリセット時は password hash だけを置き換えず、既存のログインセッションを revoke する。",[16,8462,8463,8464,8467,8468,8471],{},"rate limit はデータベース内で送信資格を ",[20,8465,8466],{},"atomically に取得","する必要があります。前回送信時刻を検索してから row を insert する実装では、二つの並行リクエストが両方とも確認を通過します。unique key と ",[25,8469,8470],{},"INSERT ... ON CONFLICT ... WHERE"," を組み合わせ、どのリクエストが送信枠を得るかをデータベースに決めさせます。",[16,8473,8474],{},"メール API の response は「このアドレスが登録済みか」を公開してはいけません。攻撃者による user enumeration を可能にします。",[81,8476,8477],{"id":8477},"より完全な本番アーキテクチャ",[16,8479,8480],{},"メール処理が Demo から本番へ進んだら、責務を分けます。",[33,8482,8485],{"className":8483,"code":8484,"language":233,"meta":38},[231],"HTTP リクエスト \u002F 業務イベント\n  → 認証、origin check、入力検証、rate limit、idempotency\n  → Queue\n  → メール template の描画\n  → EMAIL binding\n  → messageId \u002F error code を D1 へ保存\n  → Email Service logs と alert\n",[25,8486,8484],{"__ignoreMap":38},[16,8488,8489],{},"受信経路は次のようにできます。",[33,8491,8494],{"className":8492,"code":8493,"language":233,"meta":38},[231],"support@yourdomain.com\n  → Email Routing\n  → Email Worker\n  → 認証とサイズ確認\n  → MIME 解析\n  → 添付を R2 へ保存 \u002F D1 にチケットを作成\n  → 重い処理を Queue へ\n  → 人のメールボックスへ転送、または制約付き返信\n",[25,8495,8493],{"__ignoreMap":38},[248,8497,8499],{"id":8498},"d1-に記録するもの","D1 に記録するもの",[16,8501,8502],{},"記録に適する項目は次のとおりです。",[97,8504,8505,8508,8511,8515,8518,8521,8524],{},[100,8506,8507],{},"業務イベント ID と idempotency key。",[100,8509,8510],{},"template ID と template version。",[100,8512,8513,1410],{},[25,8514,6649],{},[100,8516,8517],{},"受信者数。必ずしも完全なアドレスを保存する必要はない。",[100,8519,8520],{},"accepted \u002F failed などアプリ側の状態。",[100,8522,8523],{},"Cloudflare error code。",[100,8525,8526],{},"作成、送信、最終更新時刻。",[16,8528,8529],{},"既定では記録しないもの：",[97,8531,8532,8535,8538,8541,8544,8547],{},[100,8533,8534],{},"plaintext の認証コード。",[100,8536,8537],{},"パスワードリセット Token。",[100,8539,8540],{},"完全なメール本文。",[100,8542,8543],{},"添付内容。",[100,8545,8546],{},"SMTP\u002FAPI Token。",[100,8548,8549],{},"デバッグのため無期限に保存する生の受信メール。",[16,8551,8552],{},"log 自体が新たな個人データ源になる可能性があります。",[248,8554,8556],{"id":8555},"email-routing-のdroppedを送信失敗と判断しない","Email Routing の「Dropped」を送信失敗と判断しない",[16,8558,8559,8560,8562,8563,8566],{},"公式ドキュメントは、",[25,8561,5913],{}," binding から送ったメールが実際には正常に配信されていても、Email Routing の集計ページで ",[20,8564,8565],{},"dropped"," と表示される場合があると明記しています。",[16,8568,8569,8570,8572],{},"送信結果は Email Sending の Metrics と Logs で確認し、",[25,8571,6649],{}," を使ってアプリログと関連付けます。Routing ページが示すのは受信 routing の処理であり、完全な送信配信 state machine ではありません。",[81,8574,8575],{"id":8575},"ローカルでのテスト",[16,8577,8578],{},"Wrangler は Email Worker のローカルイベントをサポートします。次を実行します。",[33,8580,8582],{"className":35,"code":8581,"language":37,"meta":38,"style":38},"pnpm exec wrangler dev\n",[25,8583,8584],{"__ignoreMap":38},[42,8585,8586,8588,8591,8594],{"class":44,"line":45},[42,8587,5353],{"class":48},[42,8589,8590],{"class":56}," exec",[42,8592,8593],{"class":56}," wrangler",[42,8595,8596],{"class":56}," dev\n",[16,8598,8599],{},"ローカル handler へ生メールを送ります。",[33,8601,8603],{"className":35,"code":8602,"language":37,"meta":38,"style":38},"curl -X POST \\\n  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com' \\\n  -H 'Content-Type: text\u002Fplain' \\\n  --data-binary $'From: sender@example.net\\nTo: support@example.com\\nSubject: Local test\\n\\nHello from local development.'\n",[25,8604,8605,8617,8624,8634],{"__ignoreMap":38},[42,8606,8607,8609,8612,8615],{"class":44,"line":45},[42,8608,264],{"class":48},[42,8610,8611],{"class":52}," -X",[42,8613,8614],{"class":56}," POST",[42,8616,273],{"class":52},[42,8618,8619,8622],{"class":44,"line":276},[42,8620,8621],{"class":56},"  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com'",[42,8623,273],{"class":52},[42,8625,8626,8629,8632],{"class":44,"line":748},[42,8627,8628],{"class":52},"  -H",[42,8630,8631],{"class":56}," 'Content-Type: text\u002Fplain'",[42,8633,273],{"class":52},[42,8635,8636,8639,8642,8645,8648,8650,8653,8656],{"class":44,"line":1590},[42,8637,8638],{"class":52},"  --data-binary",[42,8640,8641],{"class":56}," $'From: sender@example.net",[42,8643,8644],{"class":52},"\\n",[42,8646,8647],{"class":56},"To: support@example.com",[42,8649,8644],{"class":52},[42,8651,8652],{"class":56},"Subject: Local test",[42,8654,8655],{"class":52},"\\n\\n",[42,8657,8658],{"class":56},"Hello from local development.'\n",[16,8660,8661],{},"ローカルの送信 binding は text と HTML を確認できるファイルとして保存するため、template 検証に適しています。binary 添付のローカル simulation には個別の制限があるため、添付フローは remote test も追加してください。",[16,8663,8664],{},"少なくとも次をテストします。",[97,8666,8667,8670,8673,8676,8679,8682,8685,8688,8691,8694],{},[100,8668,8669],{},"通常の text と HTML。",[100,8671,8672],{},"非 ASCII の件名と送信者表示名。",[100,8674,8675],{},"重複リクエストと idempotency。",[100,8677,8678],{},"cooldown 完了前の認証コード再送。",[100,8680,8681],{},"大きな本文と添付の境界。",[100,8683,8684],{},"Header newline injection。",[100,8686,8687],{},"未検証の送信ドメイン、または制限された受信者。",[100,8689,8690],{},"bounce、suppression、枠、頻度の error。",[100,8692,8693],{},"一致しない受信アドレス、Catch-all、拒否経路。",[100,8695,8696],{},"HTML メール内の script、remote resource、危険な添付ファイル名。",[81,8698,8699],{"id":8699},"適する用途と適さない用途",[16,8701,8702],{},"適する用途：",[97,8704,8705,8708,8711,8714,8717,8720],{},[100,8706,8707],{},"登録認証、magic link、パスワードリセット。",[100,8709,8710],{},"注文、請求、デプロイ、監視通知。",[100,8712,8713],{},"個人ドメインのメール転送。",[100,8715,8716],{},"contact form とサポートチケット。",[100,8718,8719],{},"メールを trigger にする Agent や自動化。",[100,8721,8722],{},"SMTP サーバーの保守を減らしたい Cloudflare アプリケーション。",[16,8724,8725],{},"直接使うべきでない用途：",[97,8727,8728,8731,8734,8737,8740],{},[100,8729,8730],{},"同意のない marketing 一括送信。",[100,8732,8733],{},"unsubscribe、complaint、suppression 管理のない Newsletter。",[100,8735,8736],{},"匿名で公開する「代わりにメールを送る」endpoint。",[100,8738,8739],{},"IMAP folder、calendar、contact、複数人の shared mailbox が必要な企業メール。",[100,8741,8742],{},"任意の受信本文を、高権限ツールを持つ Agent へ直接渡すこと。",[16,8744,8745],{},"Cloudflare はメール入口と配信インフラを保守しますが、送信者 reputation、ユーザー同意、コンテンツ安全性、頻度制御、データ最小化はアプリケーション自身の責任です。",[81,8747,8748],{"id":8748},"最終的な判断",[16,8750,8751],{},"Cloudflare Email Service の最も価値ある点は「新しい SMTP provider」になったことではなく、メールを Workers、D1、R2、Queue、Agents と同じランタイム境界へ入れたことです。",[16,8753,8754],{},"受信メールは人のメールボックスへ転送するだけではなく、制約されたイベント入口になります。送信メールも、認証コードのためだけに外部 SDK と別の credential system を導入する必要がなくなります。",[16,8756,8757],{},"プロジェクトがすでに Workers 上にあるなら、次の最小経路から始めます。",[408,8759,8760,8767,8770,8773,8776],{},[100,8761,8762,8763,8766],{},"Email Routing で ",[25,8764,8765],{},"support@"," を受信する。",[100,8768,8769],{},"Worker binding で認証コードとパスワードリセットを送信する。",[100,8771,8772],{},"D1 で cooldown、一度限りの認証、最小限の log を実装する。",[100,8774,8775],{},"sender allowlist で binding 権限を制限する。",[100,8777,8778],{},"API の成功 response だけでなく、Email Sending log で配信を確認する。",[16,8780,8781],{},"外部 platform から本当に送信する必要が出たときに REST API を追加します。既存システムが SMTP client へ深く依存している場合だけ認証 SMTP を使います。",[81,8783,3185],{"id":3185},[97,8785,8786,8793,8799,8806,8813,8820,8827,8834,8840,8845,8851,8858,8865],{},[100,8787,8788],{},[387,8789,8792],{"href":8790,"rel":8791},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002F",[391],"Cloudflare Email Service 概要",[100,8794,8795],{},[387,8796,8798],{"href":4964,"rel":8797},[391],"Email Sending の設定",[100,8800,8801],{},[387,8802,8805],{"href":8803,"rel":8804},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Froute-emails\u002F",[391],"Email Routing の設定",[100,8807,8808],{},[387,8809,8812],{"href":8810,"rel":8811},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fdomains\u002F",[391],"ドメインと DNS 設定",[100,8814,8815],{},[387,8816,8819],{"href":8817,"rel":8818},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fsend-bindings\u002F",[391],"送信 binding の権限設定",[100,8821,8822],{},[387,8823,8826],{"href":8824,"rel":8825},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fworkers-api\u002F",[391],"Workers 送信 API",[100,8828,8829],{},[387,8830,8833],{"href":8831,"rel":8832},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Froute-emails\u002Femail-handler\u002F",[391],"Email Worker 受信 API",[100,8835,8836],{},[387,8837,6045],{"href":8838,"rel":8839},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Frest-api\u002F",[391],[100,8841,8842],{},[387,8843,8254],{"href":4970,"rel":8844},[391],[100,8846,8847],{},[387,8848,8850],{"href":6178,"rel":8849},[391],"プラットフォーム制限",[100,8852,8853],{},[387,8854,8857],{"href":8855,"rel":8856},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Fpricing\u002F",[391],"料金",[100,8859,8860],{},[387,8861,8864],{"href":8862,"rel":8863},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconcepts\u002Femail-authentication\u002F",[391],"メール認証と配信",[100,8866,8867],{},[387,8868,8871],{"href":8869,"rel":8870},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Flocal-development\u002Frouting\u002F",[391],"Email Worker のローカルテスト",[744,8873,8874],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":38,"searchDepth":748,"depth":748,"links":8876},[8877,8878,8882,8887,8893,8897,8898,8899,8900,8904,8905,8906,8907],{"id":6595,"depth":276,"text":6596},{"id":6682,"depth":276,"text":6683,"children":8879},[8880,8881],{"id":6689,"depth":748,"text":6690},{"id":6719,"depth":748,"text":6720},{"id":6746,"depth":276,"text":6747,"children":8883},[8884,8885],{"id":6753,"depth":748,"text":6754},{"id":6774,"depth":748,"text":8886},"送信は cf-bounce record を使う",{"id":6832,"depth":276,"text":6833,"children":8888},[8889,8891,8892],{"id":6858,"depth":748,"text":8890},"email() が受信メールの入口",{"id":7323,"depth":748,"text":7324},{"id":7484,"depth":748,"text":7485},{"id":7541,"depth":276,"text":7542,"children":8894},[8895,8896],{"id":7657,"depth":748,"text":7658},{"id":7870,"depth":748,"text":7871},{"id":8208,"depth":276,"text":8209},{"id":8291,"depth":276,"text":8291},{"id":8425,"depth":276,"text":8425},{"id":8477,"depth":276,"text":8477,"children":8901},[8902,8903],{"id":8498,"depth":748,"text":8499},{"id":8555,"depth":748,"text":8556},{"id":8575,"depth":276,"text":8575},{"id":8699,"depth":276,"text":8699},{"id":8748,"depth":276,"text":8748},{"id":3185,"depth":276,"text":3185},"2026 年 7 月時点の Cloudflare Email Service を使った受信、転送、解析、返信、トランザクションメール送信を、Nuxt、Workers、D1、セキュリティ、レート制限とともに解説します。",{"src":8910,"alt":8911,"caption":8912,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Fcover-v1.webp","世界中のエッジネットワークにある Worker ノードがメールを受信、認証、送信する","Cloudflare Email Service は、メールを保守が必要なサーバーからコードで処理できるイベントへ変えます",1600,900,{},"\u002Fblog\u002Fcloudflare-email-service-send-receive-guide","2026-07-28",{"title":6534,"description":8908},"blog\u002Fcloudflare-email-service-send-receive-guide",[2654,6529,8921,8922],"Workers","Serverless",{"src":8924,"poster":8910,"title":8925,"description":8926,"duration":8927,"width":3317,"height":3318},"\u002Fmedia\u002Fvideos\u002Fcloudflare-email-service-explained\u002Fcloudflare-email-service-explained-v1.mp4","Cloudflare Email Service 実践—11 分で分かる設定ガイド","DNS、Email Routing、Email Worker、send_email binding から REST、SMTP、Nuxt、D1、5 分間の認証コード、本番確認までを解説します。",661,"qW1O2KtlO0gzPc1CCQSN2y2VBaCtC21jxaEJwSq98fI",{"id":8930,"title":8931,"accent":11,"body":8932,"description":10623,"draft":775,"experience":776,"extension":777,"featured":778,"image":10624,"meta":10628,"navigation":778,"path":10629,"publishedAt":10630,"readingTime":1720,"seo":10631,"stem":10632,"tags":10633,"updatedAt":776,"video":776,"__hash__":10637,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fvue-3-6-rc-upgrade-guide.md","Vue 3.6 RC アップグレード全解説—3.5 から Vapor Mode と alien-signals へ",{"type":13,"value":8933,"toc":10584},[8934,8941,8954,8957,8966,8970,9109,9112,9117,9121,9124,9159,9162,9168,9172,9179,9182,9295,9298,9304,9309,9312,9315,9328,9335,9338,9341,9358,9361,9364,9380,9397,9400,9404,9407,9410,9418,9421,9425,9428,9551,9554,9582,9610,9613,9617,9620,9670,9673,9677,9684,9745,9748,9751,9757,9763,9766,9770,9773,9787,9795,9841,9848,9851,9864,9867,9871,9874,10009,10016,10022,10025,10112,10115,10149,10153,10166,10169,10173,10176,10179,10182,10186,10189,10193,10196,10199,10202,10206,10209,10223,10229,10246,10249,10281,10284,10288,10291,10311,10314,10318,10321,10344,10347,10351,10354,10357,10377,10380,10384,10387,10391,10394,10398,10401,10405,10408,10411,10494,10496,10499,10507,10510,10517,10520,10523,10526,10576,10581],[16,8935,8936,8937,8940],{},"バージョン番号だけを見れば、Vue 3.6 は普通のマイナーアップデートに見えます。しかしリリースノートを最後まで読むと、印象は違います。",[20,8938,8939],{},"Vue が次の段階に向けてエンジンを交換する更新","に近いのです。",[16,8942,8943,8944,437,8947,437,8950,8953],{},"一方では alien-signals を基盤にリアクティビティコアが再構築され、公開されている ",[25,8945,8946],{},"ref",[25,8948,8949],{},"computed",[25,8951,8952],{},"watch"," の使い方はほぼ変わりません。もう一方では Vapor Mode が正式に RC へ入り、Vue の SFC を Virtual DOM を経由せず、より直接的な DOM 更新ロジックへコンパイルできるようになります。",[16,8955,8956],{},"最初に、最も重要なバージョン情報を確認します。",[1355,8958,8959],{},[16,8960,8961,8962,8965],{},"2026 年 7 月 26 日時点で、Vue 3.6 の最新版は ",[20,8963,8964],{},"3.6.0-rc.2"," です。RC はリリース候補であり、安定版ではありません。この記事は評価、実験、移行準備のためのもので、すべての本番プロジェクトが直ちにアップグレードすべきだという意味ではありません。",[81,8967,8969],{"id":8968},"先に結論36-では何が変わったのか","先に結論：3.6 では何が変わったのか",[141,8971,8972,8988],{},[144,8973,8974],{},[147,8975,8976,8979,8982,8985],{},[150,8977,8978],{},"比較項目",[150,8980,8981],{},"Vue 3.5",[150,8983,8984],{},"Vue 3.6 RC",[150,8986,8987],{},"既存プロジェクトへの実際の影響",[160,8989,8990,9004,9018,9032,9046,9060,9077,9095],{},[147,8991,8992,8995,8998,9001],{},[165,8993,8994],{},"リアクティビティ",[165,8996,8997],{},"3.5 で内部最適化済み",[165,8999,9000],{},"alien-signals を基盤に大幅再構築",[165,9002,9003],{},"通常の Composition API は書き直し不要ですが、複雑な副作用とスケジューリングは回帰テストが必要",[147,9005,9006,9009,9012,9015],{},[165,9007,9008],{},"デフォルト描画",[165,9010,9011],{},"Virtual DOM",[165,9013,9014],{},"引き続き Virtual DOM",[165,9016,9017],{},"依存関係を更新するだけで Vapor へ移行することはない",[147,9019,9020,9023,9026,9029],{},[165,9021,9022],{},"Vapor Mode",[165,9024,9025],{},"3.5 の正式機能としては未提供",[165,9027,9028],{},"機能セットが RC、完全な明示的 opt-in",[165,9030,9031],{},"性能が重要な一領域から実験できる",[147,9033,9034,9037,9040,9043],{},[165,9035,9036],{},"アプリサイズ",[165,9038,9039],{},"VDOM runtime を既定で含む",[165,9041,9042],{},"純粋な Vapor アプリは VDOM runtime を省略可能",[165,9044,9045],{},"純 Vapor または境界が明確な領域でサイズ効果を得やすい",[147,9047,9048,9051,9054,9057],{},[165,9049,9050],{},"API 対応範囲",[165,9052,9053],{},"Vue 3 API の完全なエコシステム",[165,9055,9056],{},"Vapor は Vue API のサブセットをサポート",[165,9058,9059],{},"Options API やインスタンスプロキシ関連機能は直接移行できない",[147,9061,9062,9065,9068,9074],{},[165,9063,9064],{},"VDOM \u002F Vapor 混在",[165,9066,9067],{},"対象外",[165,9069,9070,9073],{},[25,9071,9072],{},"vaporInteropPlugin"," で相互運用",[165,9075,9076],{},"標準的な props、イベント、slots は対応済みですが、複雑なネストはテストが必要",[147,9078,9079,9082,9085,9092],{},[165,9080,9081],{},"イベント委譲",[165,9083,9084],{},"ネイティブなリスナー動作",[165,9086,9087,9088,9091],{},"rc.2 以降は直接 binding が既定、",[25,9089,9090],{},".delegate"," で明示的に委譲",[165,9093,9094],{},"rc.1 の自動委譲という認識を引き継がない",[147,9096,9097,9100,9103,9106],{},[165,9098,9099],{},"本番利用",[165,9101,9102],{},"安定版",[165,9104,9105],{},"現在もプレリリース",[165,9107,9108],{},"安定版と上位エコシステムの明示的対応を待つべき",[16,9110,9111],{},"一言でまとめると、",[16,9113,9114],{},[20,9115,9116],{},"Vue 3.5 の中心は開発体験と SSR 機能の向上、Vue 3.6 の中心はリアクティビティコアと描画アーキテクチャの更新です。",[81,9118,9120],{"id":9119},"_35-から-36-へ変化の重心が移った","3.5 から 3.6 へ：変化の重心が移った",[16,9122,9123],{},"Vue 3.5 には破壊的変更がなく、多くの改善を業務コードから直接感じられました。",[97,9125,9126,9129,9135,9141,9147,9150,9156],{},[100,9127,9128],{},"Reactive Props Destructure が安定版になった。",[100,9130,9131,9134],{},[25,9132,9133],{},"useTemplateRef()"," が template ref の整理方法を改善した。",[100,9136,9137,9140],{},[25,9138,9139],{},"onWatcherCleanup()"," で watcher の cleanup を自然に書けるようになった。",[100,9142,9143,9146],{},[25,9144,9145],{},"useId()"," が SSR に適した安定 ID を提供した。",[100,9148,9149],{},"非同期コンポーネントが Lazy Hydration に対応した。",[100,9151,9152,9155],{},[25,9153,9154],{},"data-allow-mismatch"," で許容する hydration 差異を宣言できるようになった。",[100,9157,9158],{},"Deferred Teleport とカスタム要素が強化された。",[16,9160,9161],{},"3.6 にも多くの修正がありますが、最も注目すべき alien-signals と Vapor は、どちらもフレームワークの下層にあります。業務コードに新 API を一つも追加しなくても、性能特性、コンパイル結果、ランタイム境界は変わります。",[16,9163,9164,9165],{},"そのためアップグレード方法も変わります。",[20,9166,9167],{},"「コンパイルできるか」だけでなく、リアクティブな動作、SSR hydration、コンポーネント間の相互運用、実際の性能を確認してください。",[81,9169,9171],{"id":9170},"変更点-1リアクティビティコアが-alien-signals-へ","変更点 1：リアクティビティコアが alien-signals へ",[16,9173,9174,9175,9178],{},"Vue 3.6 は ",[25,9176,9177],{},"@vue\u002Freactivity"," を大規模に再構築し、その基盤には StackBlitz がオープンソース化した alien-signals が使われています。",[16,9180,9181],{},"大多数のアプリケーション開発者にとって、次のコードはこれまでどおりの Vue です。",[33,9183,9185],{"className":5396,"code":9184,"language":5398,"meta":38,"style":38},"import { computed, ref, watchEffect } from 'vue'\n\nconst price = ref(99)\nconst count = ref(2)\nconst total = computed(() => price.value * count.value)\n\nwatchEffect(() => {\n  console.log(total.value)\n})\n",[25,9186,9187,9199,9203,9222,9240,9266,9270,9281,9291],{"__ignoreMap":38},[42,9188,9189,9191,9194,9196],{"class":44,"line":45},[42,9190,5405],{"class":279},[42,9192,9193],{"class":1788}," { computed, ref, watchEffect } ",[42,9195,5411],{"class":279},[42,9197,9198],{"class":56}," 'vue'\n",[42,9200,9201],{"class":44,"line":276},[42,9202,1587],{"emptyLinePlaceholder":778},[42,9204,9205,9207,9210,9212,9215,9217,9220],{"class":44,"line":748},[42,9206,5423],{"class":279},[42,9208,9209],{"class":52}," price",[42,9211,5429],{"class":279},[42,9213,9214],{"class":48}," ref",[42,9216,5622],{"class":1788},[42,9218,9219],{"class":52},"99",[42,9221,5791],{"class":1788},[42,9223,9224,9226,9229,9231,9233,9235,9238],{"class":44,"line":1590},[42,9225,5423],{"class":279},[42,9227,9228],{"class":52}," count",[42,9230,5429],{"class":279},[42,9232,9214],{"class":48},[42,9234,5622],{"class":1788},[42,9236,9237],{"class":52},"2",[42,9239,5791],{"class":1788},[42,9241,9242,9244,9247,9249,9252,9255,9257,9260,9263],{"class":44,"line":1609},[42,9243,5423],{"class":279},[42,9245,9246],{"class":52}," total",[42,9248,5429],{"class":279},[42,9250,9251],{"class":48}," computed",[42,9253,9254],{"class":1788},"(() ",[42,9256,7975],{"class":279},[42,9258,9259],{"class":1788}," price.value ",[42,9261,9262],{"class":279},"*",[42,9264,9265],{"class":1788}," count.value)\n",[42,9267,9268],{"class":44,"line":1624},[42,9269,1587],{"emptyLinePlaceholder":778},[42,9271,9272,9275,9277,9279],{"class":44,"line":1633},[42,9273,9274],{"class":48},"watchEffect",[42,9276,9254],{"class":1788},[42,9278,7975],{"class":279},[42,9280,5930],{"class":1788},[42,9282,9283,9286,9288],{"class":44,"line":1646},[42,9284,9285],{"class":1788},"  console.",[42,9287,7075],{"class":48},[42,9289,9290],{"class":1788},"(total.value)\n",[42,9292,9293],{"class":44,"line":1651},[42,9294,5529],{"class":1788},[16,9296,9297],{},"変化したのは内部です。依存関係の構築方法、状態のマーク方法、computed がいつ無効になるか、effect が依存グラフをどう伝播するか、そして関係をどう cleanup し再利用するかが変わりました。",[16,9299,9300],{},[1994,9301],{"alt":9302,"src":9303},"アクティブな経路と休眠中の経路から構成される細粒度リアクティブ依存ネットワーク","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Freactivity-core-v1.webp",[16,9305,9306],{},[2001,9307,9308],{},"この図は依存伝播の概念を示したもので、Vue の内部データ構造をフィールド単位で表したものではありません。",[248,9310,9311],{"id":9311},"性能数値をどう理解するか",[16,9313,9314],{},"Vue core の alien-signals マージ PR では、二種類のマイクロベンチマーク結果が示されています。",[97,9316,9317,9325],{},[100,9318,9319,9320,437,9322,9324],{},"多数の ",[25,9321,8946],{},[25,9323,8949],{},"、effect インスタンスを生成する場面で、サンプルのメモリ使用量が約 2.3 MB から約 2.0 MB へ減り、およそ 13% 削減された。",[100,9326,9327],{},"一つの source が変化した後、多数の computed を読み取る特定のストレスケースで、30 倍を超える性能向上が見られた。",[16,9329,9330,9331,9334],{},"二つ目の数字は目を引きますが、「Vue 3.6 のアプリが 30 倍速くなる」という意味ではありません。示しているのは、",[20,9332,9333],{},"以前は規模とともにコストが急増した一部の依存グラフで、新しい伝播アルゴリズムの計算量特性が改善した","ということです。",[16,9336,9337],{},"実際のページには DOM、layout、network、コンポーネントライブラリ、serialization、業務計算も含まれます。アップグレード前後は、自分のプロジェクトの flame chart、操作遅延、メモリ曲線、bundle 結果で判断し、フレームワークのマイクロベンチマークをそのままアプリの約束にしないでください。",[248,9339,9340],{"id":9340},"効果を感じやすいプロジェクト",[97,9342,9343,9346,9349,9352,9355],{},[100,9344,9345],{},"多数の computed が相互依存するデータダッシュボード。",[100,9347,9348],{},"高頻度の変更、フィルター、多くの派生状態を持つエディター。",[100,9350,9351],{},"長時間動作し、多数の effect を生成・破棄するワークベンチ。",[100,9353,9354],{},"フィールド数が多く、検証規則と連動関係が複雑なフォーム。",[100,9356,9357],{},"Vue のリアクティブ primitive を基盤にした状態管理やツールライブラリ。",[16,9359,9360],{},"通常のコンテンツページも内部改善を受けますが、ユーザーが大きな違いを感じるとは限りません。",[248,9362,9363],{"id":9363},"特に注意すべきコード",[16,9365,9366,9367,437,9369,437,9372,437,9374,437,9376,9379],{},"公開 API の ",[25,9368,8946],{},[25,9370,9371],{},"reactive",[25,9373,8949],{},[25,9375,8952],{},[25,9377,9378],{},"effectScope"," だけを使っていれば、移行リスクは比較的限定的です。本当に注意すべきなのは次のコードです。",[97,9381,9382,9385,9388,9391,9394],{},[100,9383,9384],{},"Vue の非公開リアクティブフィールドを直接読む、または変更する。",[100,9386,9387],{},"非公開の effect、Dep、scheduler の実装詳細へ依存する。",[100,9389,9390],{},"リアクティブ関数を monkey patch する。",[100,9392,9393],{},"watcher の実行順、同期 flush、cleanup timing に暗黙の仮定がある。",[100,9395,9396],{},"複数の Vue マイナーバージョンに対応するライブラリで、境界テストがない。",[16,9398,9399],{},"内部再構築の意義は、公開 API を保ったまま Vue が実装を交換できることです。逆に、内部実装に依存するコードほど、この種のアップグレードで問題が表面化します。",[81,9401,9403],{"id":9402},"変更点-2vapor-mode-は実験的な概念だけではなくなった","変更点 2：Vapor Mode は実験的な概念だけではなくなった",[16,9405,9406],{},"Vapor Mode は Vue SFC の新しいコンパイルモードです。従来の Vue template は VNode を生成し、Virtual DOM runtime が新旧ツリーを比較して差分を実 DOM へ反映します。Vapor ではコンパイラが動的部分を事前に認識し、より直接的で細粒度な更新ロジックを生成します。",[16,9408,9409],{},"目標は二つです。",[408,9411,9412,9415],{},[100,9413,9414],{},"アプリケーションの基礎 bundle サイズを削減する。",[100,9416,9417],{},"実行時に VNode を生成、走査、比較するコストを減らす。",[16,9419,9420],{},"公式リリースノートによると、Vapor は第三者の js-framework-benchmark で Solid や Svelte 5 に近い水準に達しています。これも「ベンチマークの場面での結果」と理解すべきで、異なるフレームワークと実アプリの性能を一つの順位に圧縮するものではありません。",[248,9422,9424],{"id":9423},"アップグレードしても-vapor-は自動で有効にならない","アップグレードしても Vapor は自動で有効にならない",[16,9426,9427],{},"Vue 3.6 も既定では VDOM を使います。SFC で Vapor を使うには明示的に指定します。",[33,9429,9433],{"className":9430,"code":9431,"language":9432,"meta":38,"style":38},"language-vue shiki shiki-themes github-dark github-dark","\u003Cscript setup vapor lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"count++\">\n    Count: {{ count }}\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","vue",[25,9434,9435,9458,9469,9473,9489,9498,9502,9511,9529,9534,9543],{"__ignoreMap":38},[42,9436,9437,9439,9442,9445,9448,9451,9453,9456],{"class":44,"line":45},[42,9438,6940],{"class":1788},[42,9440,9441],{"class":2052},"script",[42,9443,9444],{"class":48}," setup",[42,9446,9447],{"class":48}," vapor",[42,9449,9450],{"class":48}," lang",[42,9452,1832],{"class":1788},[42,9454,9455],{"class":56},"\"ts\"",[42,9457,7203],{"class":1788},[42,9459,9460,9462,9465,9467],{"class":44,"line":276},[42,9461,5405],{"class":279},[42,9463,9464],{"class":1788}," { ref } ",[42,9466,5411],{"class":279},[42,9468,9198],{"class":56},[42,9470,9471],{"class":44,"line":748},[42,9472,1587],{"emptyLinePlaceholder":778},[42,9474,9475,9477,9479,9481,9483,9485,9487],{"class":44,"line":1590},[42,9476,5423],{"class":279},[42,9478,9228],{"class":52},[42,9480,5429],{"class":279},[42,9482,9214],{"class":48},[42,9484,5622],{"class":1788},[42,9486,3571],{"class":52},[42,9488,5791],{"class":1788},[42,9490,9491,9494,9496],{"class":44,"line":1609},[42,9492,9493],{"class":1788},"\u003C\u002F",[42,9495,9441],{"class":2052},[42,9497,7203],{"class":1788},[42,9499,9500],{"class":44,"line":1624},[42,9501,1587],{"emptyLinePlaceholder":778},[42,9503,9504,9506,9509],{"class":44,"line":1633},[42,9505,6940],{"class":1788},[42,9507,9508],{"class":2052},"template",[42,9510,7203],{"class":1788},[42,9512,9513,9516,9519,9522,9524,9527],{"class":44,"line":1646},[42,9514,9515],{"class":1788},"  \u003C",[42,9517,9518],{"class":2052},"button",[42,9520,9521],{"class":48}," @click",[42,9523,1832],{"class":1788},[42,9525,9526],{"class":56},"\"count++\"",[42,9528,7203],{"class":1788},[42,9530,9531],{"class":44,"line":1651},[42,9532,9533],{"class":1788},"    Count: {{ count }}\n",[42,9535,9536,9539,9541],{"class":44,"line":1674},[42,9537,9538],{"class":1788},"  \u003C\u002F",[42,9540,9518],{"class":2052},[42,9542,7203],{"class":1788},[42,9544,9545,9547,9549],{"class":44,"line":1680},[42,9546,9493],{"class":1788},[42,9548,9508],{"class":2052},[42,9550,7203],{"class":1788},[16,9552,9553],{},"同等の入口がほかに二つあります。",[33,9555,9557],{"className":9430,"code":9556,"language":9432,"meta":38,"style":38},"\u003Cscript vapor>\n\u002F\u002F \u003Cscript setup vapor> の短縮形\n\u003C\u002Fscript>\n",[25,9558,9559,9569,9574],{"__ignoreMap":38},[42,9560,9561,9563,9565,9567],{"class":44,"line":45},[42,9562,6940],{"class":1788},[42,9564,9441],{"class":2052},[42,9566,9447],{"class":48},[42,9568,7203],{"class":1788},[42,9570,9571],{"class":44,"line":276},[42,9572,9573],{"class":8054},"\u002F\u002F \u003Cscript setup vapor> の短縮形\n",[42,9575,9576,9578,9580],{"class":44,"line":748},[42,9577,9493],{"class":1788},[42,9579,9441],{"class":2052},[42,9581,7203],{"class":1788},[33,9583,9585],{"className":9430,"code":9584,"language":9432,"meta":38,"style":38},"\u003Ctemplate vapor>\n  \u003C!-- SFC 全体を Vapor でコンパイル -->\n\u003C\u002Ftemplate>\n",[25,9586,9587,9597,9602],{"__ignoreMap":38},[42,9588,9589,9591,9593,9595],{"class":44,"line":45},[42,9590,6940],{"class":1788},[42,9592,9508],{"class":2052},[42,9594,9447],{"class":48},[42,9596,7203],{"class":1788},[42,9598,9599],{"class":44,"line":276},[42,9600,9601],{"class":8054},"  \u003C!-- SFC 全体を Vapor でコンパイル -->\n",[42,9603,9604,9606,9608],{"class":44,"line":748},[42,9605,9493],{"class":1788},[42,9607,9508],{"class":2052},[42,9609,7203],{"class":1788},[16,9611,9612],{},"つまり 3.6 へアップグレードした後も既存の VDOM アプリを維持し、特定のコンポーネントやページだけを選んで移行できます。",[248,9614,9616],{"id":9615},"純粋な-vapor-アプリケーション","純粋な Vapor アプリケーション",[16,9618,9619],{},"アプリケーションが完全に Vapor コンポーネントで構成される場合は、次を使えます。",[33,9621,9623],{"className":5396,"code":9622,"language":5398,"meta":38,"style":38},"import { createVaporApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateVaporApp(App).mount('#app')\n",[25,9624,9625,9636,9648,9652],{"__ignoreMap":38},[42,9626,9627,9629,9632,9634],{"class":44,"line":45},[42,9628,5405],{"class":279},[42,9630,9631],{"class":1788}," { createVaporApp } ",[42,9633,5411],{"class":279},[42,9635,9198],{"class":56},[42,9637,9638,9640,9643,9645],{"class":44,"line":276},[42,9639,5405],{"class":279},[42,9641,9642],{"class":1788}," App ",[42,9644,5411],{"class":279},[42,9646,9647],{"class":56}," '.\u002FApp.vue'\n",[42,9649,9650],{"class":44,"line":748},[42,9651,1587],{"emptyLinePlaceholder":778},[42,9653,9654,9657,9660,9663,9665,9668],{"class":44,"line":1590},[42,9655,9656],{"class":48},"createVaporApp",[42,9658,9659],{"class":1788},"(App).",[42,9661,9662],{"class":48},"mount",[42,9664,5622],{"class":1788},[42,9666,9667],{"class":56},"'#app'",[42,9669,5791],{"class":1788},[16,9671,9672],{},"このモードは Virtual DOM runtime を取り込む必要がなく、基礎 bundle サイズの効果を最も得やすい方法です。小さな新規アプリ、インタラクティブなツール、技術スタックを明確に制約できる独立ページに向いています。",[248,9674,9676],{"id":9675},"既存の-vdom-アプリケーションで-vapor-を使う","既存の VDOM アプリケーションで Vapor を使う",[16,9678,9679,9680,9683],{},"既存の ",[25,9681,9682],{},"createApp()"," プロジェクトには相互運用プラグインが必要です。",[33,9685,9687],{"className":5396,"code":9686,"language":5398,"meta":38,"style":38},"import { createApp, vaporInteropPlugin } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .use(vaporInteropPlugin)\n  .mount('#app')\n",[25,9688,9689,9700,9710,9714,9722,9733],{"__ignoreMap":38},[42,9690,9691,9693,9696,9698],{"class":44,"line":45},[42,9692,5405],{"class":279},[42,9694,9695],{"class":1788}," { createApp, vaporInteropPlugin } ",[42,9697,5411],{"class":279},[42,9699,9198],{"class":56},[42,9701,9702,9704,9706,9708],{"class":44,"line":276},[42,9703,5405],{"class":279},[42,9705,9642],{"class":1788},[42,9707,5411],{"class":279},[42,9709,9647],{"class":56},[42,9711,9712],{"class":44,"line":748},[42,9713,1587],{"emptyLinePlaceholder":778},[42,9715,9716,9719],{"class":44,"line":1590},[42,9717,9718],{"class":48},"createApp",[42,9720,9721],{"class":1788},"(App)\n",[42,9723,9724,9727,9730],{"class":44,"line":1609},[42,9725,9726],{"class":1788},"  .",[42,9728,9729],{"class":48},"use",[42,9731,9732],{"class":1788},"(vaporInteropPlugin)\n",[42,9734,9735,9737,9739,9741,9743],{"class":44,"line":1624},[42,9736,9726],{"class":1788},[42,9738,9662],{"class":48},[42,9740,5622],{"class":1788},[42,9742,9667],{"class":56},[42,9744,5791],{"class":1788},[16,9746,9747],{},"逆に、Vapor アプリもこのプラグインを導入して VDOM コンポーネントを含められます。ただし VDOM runtime が再び入り、bundle サイズの利点は小さくなります。",[16,9749,9750],{},"Render Function や JSX で書いたコンポーネントも VDOM コンポーネントのままです。純 Vapor アプリで使う場合は相互運用が必要です。",[16,9752,9753],{},[1994,9754],{"alt":9755,"src":9756},"既存の VDOM 領域が明示的な相互運用境界を通じて独立した Vapor 領域へ接続される","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fvapor-boundaries-v1.webp",[16,9758,9759,9760],{},"私の提案も公式ガイダンスと同じです。",[20,9761,9762],{},"二つの描画モードを境界の明確な領域へ整理し、コンポーネント階層の各段で交互にネストしないでください。",[16,9764,9765],{},"例えば、高頻度に更新するデータキャンバスだけを Vapor ページまたは subtree にし、周囲の管理ナビゲーション、dialog、成熟したコンポーネントライブラリは VDOM のままにします。そのほうが効果、障害範囲、ロールバック経路を測定しやすくなります。",[81,9767,9769],{"id":9768},"rc2-で最も陥りやすい変更イベント委譲が明示的-opt-in-になった","rc.2 で最も陥りやすい変更：イベント委譲が明示的 opt-in になった",[16,9771,9772],{},"rc.1 の段階で Vapor を試した場合、この節は特に重要です。",[16,9774,9775,9776,9779,9780,9783,9784,9786],{},"rc.1 では条件を満たすイベントが自動的に ",[25,9777,9778],{},"document"," へ委譲されました。しかし、これはネイティブ DOM と標準 Vue の動作と異なります。祖先要素が先に ",[25,9781,9782],{},"stopPropagation()"," を呼ぶとイベントは ",[25,9785,9778],{}," へ到達せず、子要素の委譲 handler も実行されません。",[16,9788,9789,9791,9792,9794],{},[20,9790,8964],{}," 以降、Vapor は既定で DOM listener を要素へ直接 binding します。document レベルの委譲を明示的に使いたい場合だけ、Vapor 専用の ",[25,9793,9090],{}," modifier を付けます。",[33,9796,9798],{"className":9430,"code":9797,"language":9432,"meta":38,"style":38},"\u003Cbutton @click.delegate=\"onClick\">\n  保存\n\u003C\u002Fbutton>\n",[25,9799,9800,9828,9833],{"__ignoreMap":38},[42,9801,9802,9804,9806,9809,9812,9814,9817,9819,9821,9824,9826],{"class":44,"line":45},[42,9803,6940],{"class":1788},[42,9805,9518],{"class":2052},[42,9807,9808],{"class":1788}," @",[42,9810,9811],{"class":48},"click",[42,9813,1782],{"class":1788},[42,9815,9816],{"class":48},"delegate",[42,9818,1832],{"class":1788},[42,9820,1856],{"class":56},[42,9822,9823],{"class":1788},"onClick",[42,9825,1856],{"class":56},[42,9827,7203],{"class":1788},[42,9829,9830],{"class":44,"line":276},[42,9831,9832],{"class":1788},"  保存\n",[42,9834,9835,9837,9839],{"class":44,"line":748},[42,9836,9493],{"class":1788},[42,9838,9518],{"class":2052},[42,9840,7203],{"class":1788},[16,9842,9843,9844,9847],{},"古い ",[25,9845,9846],{},"compilerOptions.eventDelegation"," は削除されました。",[16,9849,9850],{},"移行では二つの作業が必要です。",[408,9852,9853,9858],{},[100,9854,9679,9855,9857],{},[25,9856,9846],{}," を削除する。",[100,9859,9860,9861,9863],{},"数が多く、イベントが安定し、伝播経路を検証済みの listener だけに ",[25,9862,9090],{}," を使う。",[16,9865,9866],{},"これは全体へ機械的に置換するための最適化ではありません。委譲に価値があるかは、ノード数、ライフサイクル、伝播セマンティクスを合わせて判断します。",[81,9868,9870],{"id":9869},"vapor-と標準-vue-の互換性境界","Vapor と標準 Vue の互換性境界",[16,9872,9873],{},"Vapor は初日から Vue の歴史的な機能をすべて覆うことを目標にしていません。公式には既存 Vue API のサブセットをサポートし、VNode やコンポーネントの公開インスタンスプロキシへ依存する機能は現在直接利用できません。",[141,9875,9876,9888],{},[144,9877,9878],{},[147,9879,9880,9882,9885],{},[150,9881,7221],{},[150,9883,9884],{},"VDOM コンポーネント",[150,9886,9887],{},"Vapor コンポーネント",[160,9889,9890,9900,9910,9921,9937,9948,9960,9972,9998],{},[147,9891,9892,9895,9897],{},[165,9893,9894],{},"Composition API",[165,9896,6244],{},[165,9898,9899],{},"主なプログラミングモデル",[147,9901,9902,9905,9907],{},[165,9903,9904],{},"Options API",[165,9906,6244],{},[165,9908,9909],{},"非対応",[147,9911,9912,9917,9919],{},[165,9913,9914],{},[25,9915,9916],{},"app.config.globalProperties",[165,9918,6244],{},[165,9920,9909],{},[147,9922,9923,9928,9931],{},[165,9924,9925],{},[25,9926,9927],{},"getCurrentInstance()",[165,9929,9930],{},"現在のインスタンスを返す",[165,9932,9933,9936],{},[25,9934,9935],{},"null"," を返す",[147,9938,9939,9942,9945],{},[165,9940,9941],{},"Render Function \u002F JSX",[165,9943,9944],{},"ネイティブ VDOM",[165,9946,9947],{},"VDOM のまま、混在には interop が必要",[147,9949,9950,9955,9957],{},[165,9951,9952],{},[25,9953,9954],{},"v-memo",[165,9956,6244],{},[165,9958,9959],{},"対象外 \u002F 非対応",[147,9961,9962,9968,9970],{},[165,9963,9964,9967],{},[25,9965,9966],{},"@vue:xxx"," 要素ライフサイクルイベント",[165,9969,6244],{},[165,9971,9909],{},[147,9973,9974,9977,9980],{},[165,9975,9976],{},"コンポーネント template ref",[165,9978,9979],{},"公開インスタンス機能を公開",[165,9981,9982,437,9985,437,9988,437,9991,437,9994,9997],{},[25,9983,9984],{},"$el",[25,9986,9987],{},"$props",[25,9989,9990],{},"$attrs",[25,9992,9993],{},"$slots",[25,9995,9996],{},"$refs"," などを公開しない",[147,9999,10000,10003,10006],{},[165,10001,10002],{},"カスタム directive",[165,10004,10005],{},"標準 directive lifecycle object",[165,10007,10008],{},"Vapor 専用の関数インターフェース",[248,10010,10012,10015],{"id":10011},"slotsdefault-を副作用のない確認に使わない",[25,10013,10014],{},"slots.default()"," を副作用のない確認に使わない",[16,10017,10018,10019,10021],{},"VDOM では、先に ",[25,10020,10014],{}," を呼び、返った内容を確認してから fallback を描画するコンポーネントがあります。Vapor で slot を呼ぶと、Block と DOM の生成、リアクティブ effect の登録、hydration 時の既存 SSR DOM の取得が直ちに行われる可能性があります。",[16,10023,10024],{},"次のように書かないでください。",[33,10026,10028],{"className":9430,"code":10027,"language":9432,"meta":38,"style":38},"\u003Cscript setup vapor>\nimport { useSlots } from 'vue'\n\nconst slots = useSlots()\nconst content = slots.default?.()\nconst showFallback = !content\n\u003C\u002Fscript>\n",[25,10029,10030,10042,10053,10057,10071,10089,10104],{"__ignoreMap":38},[42,10031,10032,10034,10036,10038,10040],{"class":44,"line":45},[42,10033,6940],{"class":1788},[42,10035,9441],{"class":2052},[42,10037,9444],{"class":48},[42,10039,9447],{"class":48},[42,10041,7203],{"class":1788},[42,10043,10044,10046,10049,10051],{"class":44,"line":276},[42,10045,5405],{"class":279},[42,10047,10048],{"class":1788}," { useSlots } ",[42,10050,5411],{"class":279},[42,10052,9198],{"class":56},[42,10054,10055],{"class":44,"line":748},[42,10056,1587],{"emptyLinePlaceholder":778},[42,10058,10059,10061,10064,10066,10069],{"class":44,"line":1590},[42,10060,5423],{"class":279},[42,10062,10063],{"class":52}," slots",[42,10065,5429],{"class":279},[42,10067,10068],{"class":48}," useSlots",[42,10070,6965],{"class":1788},[42,10072,10073,10075,10078,10080,10083,10086],{"class":44,"line":1609},[42,10074,5423],{"class":279},[42,10076,10077],{"class":52}," content",[42,10079,5429],{"class":279},[42,10081,10082],{"class":1788}," slots.",[42,10084,10085],{"class":48},"default",[42,10087,10088],{"class":1788},"?.()\n",[42,10090,10091,10093,10096,10098,10101],{"class":44,"line":1624},[42,10092,5423],{"class":279},[42,10094,10095],{"class":52}," showFallback",[42,10097,5429],{"class":279},[42,10099,10100],{"class":279}," !",[42,10102,10103],{"class":1788},"content\n",[42,10105,10106,10108,10110],{"class":44,"line":1633},[42,10107,9493],{"class":1788},[42,10109,9441],{"class":2052},[42,10111,7203],{"class":1788},[16,10113,10114],{},"実際の slot 描画は template に任せるほうが安全です。",[33,10116,10118],{"className":9430,"code":10117,"language":9432,"meta":38,"style":38},"\u003Ctemplate>\n  \u003Cslot \u002F>\n\u003C\u002Ftemplate>\n",[25,10119,10120,10128,10141],{"__ignoreMap":38},[42,10121,10122,10124,10126],{"class":44,"line":45},[42,10123,6940],{"class":1788},[42,10125,9508],{"class":2052},[42,10127,7203],{"class":1788},[42,10129,10130,10132,10135,10139],{"class":44,"line":276},[42,10131,9515],{"class":1788},[42,10133,10134],{"class":2052},"slot",[42,10136,10138],{"class":10137},"sX7ps"," \u002F",[42,10140,7203],{"class":1788},[42,10142,10143,10145,10147],{"class":44,"line":748},[42,10144,9493],{"class":1788},[42,10146,9508],{"class":2052},[42,10148,7203],{"class":1788},[248,10150,10152],{"id":10151},"カスタム-directive-は個別に移行する","カスタム directive は個別に移行する",[16,10154,10155,10156,437,10159,437,10162,10165],{},"Vapor のカスタム directive はノードとリアクティブ getter を受け取り、cleanup 関数を返せます。VDOM directive の ",[25,10157,10158],{},"mounted",[25,10160,10161],{},"updated",[25,10163,10164],{},"unmounted"," オブジェクトをそのまま移すものではありません。",[16,10167,10168],{},"多くのカスタム directive に依存するプロジェクトやコンポーネントライブラリでは、独立した移行項目として一覧化し、リアクティブ更新、cleanup、SSR 動作を一つずつ検証してください。",[81,10170,10172],{"id":10171},"nuxtrouterpiniaコンポーネントライブラリをどう見るべきか","Nuxt、Router、Pinia、コンポーネントライブラリをどう見るべきか",[16,10174,10175],{},"Vue core で利用できることは、その上のフレームワークが Vapor 統合を自動的に完了したことを意味しません。",[16,10177,10178],{},"公式 Vapor Roadmap は Router、Pinia、Nuxt、DevTools、Vue Test Utils などのエコシステム作業を個別に追跡しています。Nuxt はさらに SSR hydration 機能へ依存します。実プロジェクトでは、コンパイラの Vapor 認識、サーバー出力、クライアント hydration、開発ツールでのコンポーネント確認までが一つの完全なチェーンです。",[16,10180,10181],{},"そのため、プロジェクトを二種類に分けて考えます。",[248,10183,10185],{"id":10184},"通常の-vue-vite-アプリケーション","通常の Vue \u002F Vite アプリケーション",[16,10187,10188],{},"ブランチ上で 3.6 RC へアップグレードし、既存 VDOM モードを先に検証してから、一つの leaf ページで Vapor を試します。依存関係が比較的直接的なので、問題の原因も特定しやすくなります。",[248,10190,10192],{"id":10191},"nuxtssrまたはコンポーネントライブラリを多用するアプリ","Nuxt、SSR、またはコンポーネントライブラリを多用するアプリ",[16,10194,10195],{},"package override で下層 Vue だけを RC に変え、Vapor 移行が完了したと考えないでください。フレームワーク、build plugin、SSR チェーンが対象バージョンを明示的にサポートすることを確認してから、hydration、ルート遷移、非同期コンポーネント、コンポーネントライブラリとの相互運用をテストします。",[16,10197,10198],{},"Vapor を使わなくても、Vue 3.6 の新しいリアクティビティコアを試す価値はあります。ただし独立ブランチかテスト環境に置くのが安全です。",[81,10200,10201],{"id":10201},"より安全なアップグレード手順",[248,10203,10205],{"id":10204},"第-1-段階vapor-を有効にせず依存関係だけを更新","第 1 段階：Vapor を有効にせず、依存関係だけを更新",[16,10207,10208],{},"通常の Vue \u002F Vite プロジェクトでは、実験ブランチに RC をインストールできます。",[33,10210,10212],{"className":35,"code":10211,"language":37,"meta":38,"style":38},"pnpm add vue@3.6.0-rc.2\n",[25,10213,10214],{"__ignoreMap":38},[42,10215,10216,10218,10220],{"class":44,"line":45},[42,10217,5353],{"class":48},[42,10219,5356],{"class":56},[42,10221,10222],{"class":56}," vue@3.6.0-rc.2\n",[16,10224,10225,10228],{},[25,10226,10227],{},"@vue\u002Fserver-renderer"," に直接依存している場合は Vue と同じバージョンに揃えます。",[33,10230,10232],{"className":35,"code":10231,"language":37,"meta":38,"style":38},"pnpm add vue@3.6.0-rc.2 @vue\u002Fserver-renderer@3.6.0-rc.2\n",[25,10233,10234],{"__ignoreMap":38},[42,10235,10236,10238,10240,10243],{"class":44,"line":45},[42,10237,5353],{"class":48},[42,10239,5356],{"class":56},[42,10241,10242],{"class":56}," vue@3.6.0-rc.2",[42,10244,10245],{"class":56}," @vue\u002Fserver-renderer@3.6.0-rc.2\n",[16,10247,10248],{},"まず従来の VDOM アプリを検証します。",[97,10250,10251,10254,10257,10266,10269,10272,10275,10278],{},[100,10252,10253],{},"unit test と component test。",[100,10255,10256],{},"初期 SSR とクライアント hydration。",[100,10258,10259,10260,437,10262,437,10264,1410],{},"複雑な ",[25,10261,8952],{},[25,10263,8949],{},[25,10265,9378],{},[100,10267,10268],{},"高速なルート遷移後の副作用 cleanup。",[100,10270,10271],{},"カスタム directive、Teleport、Suspense、非同期コンポーネント。",[100,10273,10274],{},"開発 build と本番 build。",[100,10276,10277],{},"ブラウザコンソールの warning と error。",[100,10279,10280],{},"重要な操作の時間とメモリ基準。",[16,10282,10283],{},"この段階で失敗した場合、原因は Vapor よりも新しいリアクティビティ実装や RC の回帰である可能性が高く、切り分けが簡単になります。",[248,10285,10287],{"id":10286},"第-2-段階境界が明確な領域を一つ選ぶ","第 2 段階：境界が明確な領域を一つ選ぶ",[16,10289,10290],{},"良い試験領域には通常、次の条件がそろっています。",[97,10292,10293,10296,10299,10302,10305,10308],{},[100,10294,10295],{},"Composition API を使っている。",[100,10297,10298],{},"コンポーネントインスタンスへの依存が少ない。",[100,10300,10301],{},"複雑な VDOM コンポーネントライブラリへ依存しない。",[100,10303,10304],{},"更新頻度が高く、実際の性能負荷がある。",[100,10306,10307],{},"独立したルートまたは明確なコンポーネントツリー境界がある。",[100,10309,10310],{},"再現可能な性能テストがある。",[16,10312,10313],{},"移行前の JavaScript サイズ、操作遅延、CPU 時間、メモリを記録してから Vapor を有効にします。基準がなければ「技術的に動いた」を「移行する価値があった」と誤認しやすくなります。",[248,10315,10317],{"id":10316},"第-3-段階相互運用と-ssr-を検証","第 3 段階：相互運用と SSR を検証",[16,10319,10320],{},"最低でも次の組み合わせを確認します。",[408,10322,10323,10326,10329,10332,10335,10338,10341],{},[100,10324,10325],{},"VDOM 親コンポーネントに Vapor 子コンポーネント。",[100,10327,10328],{},"Vapor 親コンポーネントに VDOM 子コンポーネント。",[100,10330,10331],{},"props、emit、双方向 binding。",[100,10333,10334],{},"default、named、scoped slot。",[100,10336,10337],{},"Suspense、非同期コンポーネント、error boundary。",[100,10339,10340],{},"サーバー出力、hydration、mismatch recovery。",[100,10342,10343],{},"unmount 後の watcher、event、directive cleanup。",[16,10345,10346],{},"rc.2 の修正には hydration、Suspense、slot anchor、VDOM interop に集中する項目が多くあります。これらの領域が急速に収束している証拠であると同時に、今最も重点的にテストすべき場所でもあります。",[248,10348,10350],{"id":10349},"第-4-段階安定版を待って本番スケジュールを決める","第 4 段階：安定版を待って本番スケジュールを決める",[16,10352,10353],{},"RC は機能セットがほぼ確定し、回帰と境界問題の発見へ重点が移った状態です。エコシステム全体が安定したという意味ではありません。",[16,10355,10356],{},"本番プロジェクトは少なくとも次の条件を満たす必要があります。",[97,10358,10359,10362,10365,10368,10371,10374],{},[100,10360,10361],{},"Vue 3.6 の安定版が公開されている。",[100,10363,10364],{},"利用するフレームワークと build tool が明示的に対応している。",[100,10366,10367],{},"主要コンポーネントライブラリが相互運用テストを通過している。",[100,10369,10370],{},"DevTools、テストツール、SSR チェーンが利用できる。",[100,10372,10373],{},"性能効果が移行・保守コストを上回る。",[100,10375,10376],{},"いつでも VDOM へ戻せる。",[81,10378,10379],{"id":10379},"推奨しない四つのこと",[248,10381,10383],{"id":10382},"_1-rc-を通常の-patch-と同じように扱わない","1. RC を通常の patch と同じように扱わない",[16,10385,10386],{},"プレリリースでは動作が変わる可能性があります。rc.1 から rc.2 へのイベント委譲の変更が最も分かりやすい例です。",[248,10388,10390],{"id":10389},"_2-vapor-を使うために全コンポーネントを移行しない","2. 「Vapor を使う」ために全コンポーネントを移行しない",[16,10392,10393],{},"ナビゲーション、静的コンテンツ、更新頻度の低い設定ページでは意味のある効果を得られないかもしれません。移行率ではなく性能 bottleneck を優先します。",[248,10395,10397],{"id":10396},"_3-マイクロベンチマークをアプリの約束にしない","3. マイクロベンチマークをアプリの約束にしない",[16,10399,10400],{},"「30 倍超」は特定のリアクティブストレスケース、「Solid や Svelte 5 と同水準」は第三者 benchmark の結果です。技術的な方向性は証明しますが、プロジェクト自身の測定には代えられません。",[248,10402,10404],{"id":10403},"_4-エコシステム境界を無視しない","4. エコシステム境界を無視しない",[16,10406,10407],{},"Vue core、SFC compiler、Vite plugin、Nuxt、SSR、Router、Pinia、コンポーネントライブラリ、DevTools、テストツールが一緒に開発体験を構成します。一つの package を RC にするだけでは、チェーン全体の更新にはなりません。",[81,10409,10410],{"id":10410},"プロジェクト種別ごとの提案",[141,10412,10413,10426],{},[144,10414,10415],{},[147,10416,10417,10420,10423],{},[150,10418,10419],{},"プロジェクト種別",[150,10421,10422],{},"今試す価値",[150,10424,10425],{},"提案",[160,10427,10428,10439,10450,10461,10472,10483],{},[147,10429,10430,10433,10436],{},[165,10431,10432],{},"小規模な新規ツール、社内実験",[165,10434,10435],{},"ある",[165,10437,10438],{},"純 Vapor を試し、bundle サイズと操作性能を測る",[147,10440,10441,10444,10447],{},[165,10442,10443],{},"既存 Vue SPA",[165,10445,10446],{},"ブランチ上で試す価値がある",[165,10448,10449],{},"まず VDOM を維持し、性能が重要な一ページだけを移行",[147,10451,10452,10455,10458],{},[165,10453,10454],{},"Nuxt \u002F SSR サイト",[165,10456,10457],{},"研究には適するが本番は待つ",[165,10459,10460],{},"フレームワーク対応を確認し hydration を重点的にテスト",[147,10462,10463,10466,10469],{},[165,10464,10465],{},"コンポーネントライブラリ",[165,10467,10468],{},"早期に互換テストを始める",[165,10470,10471],{},"instance proxy、slots、カスタム directive、interop を確認",[147,10473,10474,10477,10480],{},[165,10475,10476],{},"大規模ダッシュボード \u002F エディター",[165,10478,10479],{},"stress test の価値が高い",[165,10481,10482],{},"alien-signals と Vapor それぞれの効果を分けて比較",[147,10484,10485,10488,10491],{},[165,10486,10487],{},"コンテンツ中心のサイト",[165,10489,10490],{},"優先度は低い",[165,10492,10493],{},"限定的な効果のために複雑さを増やさず、安定版と成熟したエコシステムを待つ",[81,10495,8748],{"id":8748},[16,10497,10498],{},"Vue 3.6 RC の最も価値ある点は、業務コードですぐ使える新 API の数ではなく、二つの長期的な問題を同時に進めていることです。",[97,10500,10501,10504],{},[100,10502,10503],{},"Vue のリアクティビティは、より複雑な依存グラフでも効率を保てるか。",[100,10505,10506],{},"Vue の template は、慣れた開発体験を保ちながら Virtual DOM のコストを必須ではなくせるか。",[16,10508,10509],{},"alien-signals は一つ目に答え、Vapor は二つ目に答えようとしています。",[16,10511,10512,10513,10516],{},"既存プロジェクトで最も合理的な戦略は、",[20,10514,10515],{},"まず 3.6 をリアクティビティコアの更新としてテストし、その後 Vapor を任意の局所コンパイルモードとして評価すること","です。二つを分けて検証すれば、性能変化の原因を理解でき、リスクを明確な境界内に保てます。",[16,10518,10519],{},"Vue 3.6 はまだ「全員が今日アップグレードすべき」段階ではありません。しかし「すべての Vue チームが理解を始める価値がある」段階には達しています。",[10521,10522],"hr",{},[81,10524,10525],{"id":10525},"資料と注記",[97,10527,10528,10536,10544,10552,10560,10568],{},[100,10529,10530,10535],{},[387,10531,10534],{"href":10532,"rel":10533},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fblob\u002Fminor\u002FCHANGELOG.md",[391],"Vue 3.6 minor ブランチ Changelog","：3.6.0-rc.1 の Vapor 概要と互換性境界、rc.2 のイベント委譲変更。",[100,10537,10538,10543],{},[387,10539,10542],{"href":10540,"rel":10541},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.6.0-rc.2",[391],"Vue 3.6.0-rc.2 Release","：この記事公開時点の最新リリース候補。",[100,10545,10546,10551],{},[387,10547,10550],{"href":10548,"rel":10549},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-5",[391],"Vue 3.5 リリースノート","：Vue 3.5 のリアクティビティ改善、SSR、開発者 API の基準。",[100,10553,10554,10559],{},[387,10555,10558],{"href":10556,"rel":10557},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F12349",[391],"Vue core：alien-signals 再構築 PR","：リアクティビティ実装、メモリ、特定ストレスケースのマイクロベンチマーク。",[100,10561,10562,10567],{},[387,10563,10566],{"href":10564,"rel":10565},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F13687",[391],"Vapor Mode Roadmap","：コア機能と Vue エコシステム統合のロードマップ。",[100,10569,10570,10575],{},[387,10571,10574],{"href":10572,"rel":10573},"https:\u002F\u002Fgithub.com\u002Fkrausest\u002Fjs-framework-benchmark",[391],"js-framework-benchmark","：Vue 公式リリースノートが参照した第三者フロントエンド benchmark。",[1355,10577,10578],{},[16,10579,10580],{},"この記事のアップグレード提案は、公式資料とエンジニアリング経験に基づく私の判断です。AI が資料整理と言語校正を補助しましたが、各プロジェクトでは自身のテスト結果を基準にしてください。",[744,10582,10583],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sX7ps, html code.shiki .sX7ps{--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":38,"searchDepth":748,"depth":748,"links":10585},[10586,10587,10588,10593,10598,10599,10604,10608,10614,10620,10621,10622],{"id":8968,"depth":276,"text":8969},{"id":9119,"depth":276,"text":9120},{"id":9170,"depth":276,"text":9171,"children":10589},[10590,10591,10592],{"id":9311,"depth":748,"text":9311},{"id":9340,"depth":748,"text":9340},{"id":9363,"depth":748,"text":9363},{"id":9402,"depth":276,"text":9403,"children":10594},[10595,10596,10597],{"id":9423,"depth":748,"text":9424},{"id":9615,"depth":748,"text":9616},{"id":9675,"depth":748,"text":9676},{"id":9768,"depth":276,"text":9769},{"id":9869,"depth":276,"text":9870,"children":10600},[10601,10603],{"id":10011,"depth":748,"text":10602},"slots.default() を副作用のない確認に使わない",{"id":10151,"depth":748,"text":10152},{"id":10171,"depth":276,"text":10172,"children":10605},[10606,10607],{"id":10184,"depth":748,"text":10185},{"id":10191,"depth":748,"text":10192},{"id":10201,"depth":276,"text":10201,"children":10609},[10610,10611,10612,10613],{"id":10204,"depth":748,"text":10205},{"id":10286,"depth":748,"text":10287},{"id":10316,"depth":748,"text":10317},{"id":10349,"depth":748,"text":10350},{"id":10379,"depth":276,"text":10379,"children":10615},[10616,10617,10618,10619],{"id":10382,"depth":748,"text":10383},{"id":10389,"depth":748,"text":10390},{"id":10396,"depth":748,"text":10397},{"id":10403,"depth":748,"text":10404},{"id":10410,"depth":276,"text":10410},{"id":8748,"depth":276,"text":8748},{"id":10525,"depth":276,"text":10525},"Vue 3.6.0-rc.2 時点で、Vue 3.5 と 3.6 のリアクティビティコア、Vapor Mode、イベント委譲、相互運用境界、段階的な移行戦略を比較します。",{"src":10625,"alt":10626,"caption":10627,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fcover-v1.webp","Vue 3.5 の Virtual DOM コンポーネントツリーが結晶の通路を抜け、Vue 3.6 の細粒度シグナルネットワークへ移行する","Vue 3.6 RC は新 API の一覧というより、コンパイラとランタイムのアーキテクチャを切り替える更新です",{},"\u002Fblog\u002Fvue-3-6-rc-upgrade-guide","2026-07-26",{"title":8931,"description":10623},"blog\u002Fvue-3-6-rc-upgrade-guide",[10634,10635,10636],"Vue","JavaScript","フロントエンド","Nh6ytLUqnzdHOnmVTD55dslpQ84UZ1_-faPlGdYtf_o",{"id":10639,"title":10640,"accent":3323,"body":10641,"description":10876,"draft":775,"experience":10877,"extension":777,"featured":778,"image":10882,"meta":10886,"navigation":778,"path":10887,"publishedAt":10888,"readingTime":1646,"seo":10889,"stem":10890,"tags":10891,"updatedAt":776,"video":776,"__hash__":10895,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Ffrom-gpt-4-invite-to-agents.md","一通の GPT‑4 招待メールから、AI エージェントの時代へ",{"type":13,"value":10642,"toc":10868},[10643,10646,10649,10654,10661,10667,10670,10673,10677,10680,10683,10689,10692,10695,10699,10702,10716,10719,10736,10742,10745,10748,10752,10755,10758,10772,10775,10789,10792,10796,10799,10802,10819,10822,10826,10829,10832,10835,10838,10840,10842],[16,10644,10645],{},"今日、古いメールを整理していると、2023 年 3 月 18 日に届いた一通のメールが再び目に入りました。",[16,10647,10648],{},"件名はとても簡潔でした。",[1355,10650,10651],{},[16,10652,10653],{},"You’re invited to use the OpenAI GPT-4 API.",[16,10655,10656,10657,10660],{},"本文には、",[20,10658,10659],{},"8K のコンテキスト","を持つ GPT‑4 を API から利用できるようになったと書かれていました。",[16,10662,10663],{},[1994,10664],{"alt":10665,"src":10666},"8K コンテキストを利用できると記された、2023 年の GPT‑4 API 招待メール","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Femail-invite-v1.webp",[16,10668,10669],{},"GPT‑4 の公開から、まだ数日しか経っていない頃です。API を使うには申請して待ち、OpenAI から段階的に招待される必要がありました。公開時の資料では、標準版 GPT‑4 のコンテキスト長は 8,192 トークンで、32,768 トークン版は限定的に提供されていました。当時の私には、API の利用権そのものが、開いたばかりの小さな扉のように感じられました。",[16,10671,10672],{},"3 年以上が過ぎた今、このメールは製品のお知らせというより、一つのタイムカプセルに見えます。",[81,10674,10676],{"id":10675},"数字が大きくなったことはもっとも見えやすい変化にすぎない","数字が大きくなったことは、もっとも見えやすい変化にすぎない",[16,10678,10679],{},"初期の開発者にとって、4K や 8K のコンテキストは身近な制約でした。プロンプトを書くたびに残りの容量を数え、資料を削り、長い文章を分割し、前のリクエストの結果を次へ引き継ぐ方法を考えていました。",[16,10681,10682],{},"2026 年になると、100 万トークン規模のコンテキストは最先端モデルの主流に入りました。OpenAI の現在のモデル一覧では、GPT‑5.6 系列に約 105 万トークンのコンテキストウィンドウが記載されています。Google の長文コンテキスト資料でも、複数の Gemini モデルが 100 万トークン以上に対応すると説明されています。",[16,10684,10685],{},[1994,10686],{"alt":10687,"src":10688},"4K、8K から 100 万トークン以上へ。短いメモがマルチモーダルな資料回廊へ伸びていく","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcontext-window-v1.webp",[16,10690,10691],{},"これは単に「より多くの文章を詰め込める」という話ではありません。コード、画像、音声、動画、大量の文書を、一つのタスクの背景として扱えるようになりました。以前なら分割、索引化、要約が必要だった資料を、同じ作業コンテキストに置ける可能性が広がっています。",[16,10693,10694],{},"しかし、長いコンテキストは完全な記憶ではなく、モデルが自動的に重要な点を見抜く保証でもありません。資料が増えれば、ノイズ、コスト、遅延、誤った関連付けも増えます。重要なのは今も、どの情報を選び、どの順序で渡し、モデルが重要な制約を理解したかをどう検証するかです。",[81,10696,10698],{"id":10697},"本当に変わったのはやり取りの単位","本当に変わったのは、やり取りの単位",[16,10700,10701],{},"3 年前、私がよく行っていたのは「質問と回答」でした。",[408,10703,10704,10707,10710,10713],{},[100,10705,10706],{},"プロンプトを書く。",[100,10708,10709],{},"モデルの生成を待つ。",[100,10711,10712],{},"結果をコピーする。",[100,10714,10715],{},"残りの作業を自分で終える。",[16,10717,10718],{},"今では、より多くのタスクが「目標、文脈、行動、検証」という形になっています。",[408,10720,10721,10724,10727,10730,10733],{},[100,10722,10723],{},"最終目標と完了条件を示す。",[100,10725,10726],{},"リポジトリ、文書、現在の状態を渡す。",[100,10728,10729],{},"検索、ファイル、ターミナルなどのツール利用を許可する。",[100,10731,10732],{},"相互に依存する一連の手順を実行させる。",[100,10734,10735],{},"テスト、ビルド、差分確認、人のレビューで結果を確かめる。",[16,10737,10738],{},[1994,10739],{"alt":10740,"src":10741},"チャット欄が文書、コード、ブラウザ、ツール呼び出し、人の確認を含むエージェントのワークフローへ広がる","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fagent-workflow-v1.webp",[16,10743,10744],{},"OpenAI の現在のモデルおよび Responses API の資料には、関数呼び出し、ウェブ検索、ファイル検索、コンピューター操作が利用可能なツールとして記載されています。モデルは「どうすればよいか」を説明するだけでなく、与えられた権限の範囲で実際の作業に参加し始めました。",[16,10746,10747],{},"これが、私の考える「チャットモデル」と「エージェント」のもっとも重要な違いです。前者はコンテンツを返し、後者は状態が変化するタスクを前へ進めます。",[81,10749,10751],{"id":10750},"プログラマーの仕事は消えず両端へ移動した","プログラマーの仕事は消えず、両端へ移動した",[16,10753,10754],{},"モデルがコードを読み、ファイルを編集し、コマンドを実行し、結果を確認できるようになっても、プログラマーが不要になるわけではありません。むしろ私たちの注意は、タスクの前後へ移っていきます。",[16,10756,10757],{},"行動の前には、問題をより明確に定義する必要があります。",[97,10759,10760,10763,10766,10769],{},[100,10761,10762],{},"目標は何か。何が対象外なのか。",[100,10764,10765],{},"どの資料を信頼でき、どれは参考にすぎないのか。",[100,10767,10768],{},"どの権限を渡せるか。どの操作は確認を必要とするか。",[100,10770,10771],{},"失敗したとき、停止、再試行、ロールバックのどれを選ぶか。",[16,10773,10774],{},"行動の後には、結果をより確実に検証する必要があります。",[97,10776,10777,10780,10783,10786],{},[100,10778,10779],{},"コードはテストとビルドを通過するか。",[100,10781,10782],{},"ページは本当に期待どおりか。「生成できた」だけではないか。",[100,10784,10785],{},"外部の状態は実際に変わったか。",[100,10787,10788],{},"結果を説明、再現、監査できるか。",[16,10790,10791],{},"3 年前、私たちはより良いプロンプトの書き方を学んでいました。今はそれに加えて、文脈、ツール、権限、チェックポイント、受け入れ条件の設計も学ぶ必要があります。",[81,10793,10795],{"id":10794},"能力は委ねられても責任は外部化できない","能力は委ねられても、責任は外部化できない",[16,10797,10798],{},"エージェントが現実の操作を実行できるほど、境界は重要になります。",[16,10800,10801],{},"記事の要約を頼むことと、本番データベースの変更を許可することは、同じ権限ではありません。メールの下書きを頼むことと、顧客へ直接送信させることも、同じリスクではありません。成熟したエージェントシステムは「何ができるか」だけでなく、次の点を明確にするべきです。",[97,10803,10804,10807,10810,10813,10816],{},[100,10805,10806],{},"デフォルトは読み取り専用か、書き込み可能か。",[100,10808,10809],{},"実行前に人の確認が必要な操作は何か。",[100,10811,10812],{},"対象、ディレクトリ、アカウント、データ範囲をどう制限するか。",[100,10814,10815],{},"必要な操作履歴をどう保存するか。",[100,10817,10818],{},"問題が起きたとき、どう停止し復旧するか。",[16,10820,10821],{},"モデルがより多くの行動を担っても、判断と責任は消えません。なぜ行うのか、根拠は何か、誰がリスクを負うのか、結果を受け入れられるかという問いには、最後まで人が答える必要があります。",[81,10823,10825],{"id":10824},"_3-年後私たちは今日をどう見るだろう","3 年後、私たちは今日をどう見るだろう",[16,10827,10828],{},"この古いメールは、偶然残しておいた一枚の切符のようです。",[16,10830,10831],{},"2023 年、私が申請していたのはモデルへアクセスする機会でした。2026 年には、長期目標を理解させ、ツールを使わせ、複数の工程を進め、人の監督のもとで仕事を完了させる方法を学んでいます。",[16,10833,10834],{},"心を動かされるのは、パラメーターが何倍になったかではなく、時間が圧縮されたことです。遠い未来にしかないと思われた能力は、何十年もかけて少しずつ来たのではありません。数回のバージョン更新の間に、静かに日常へ入り込みました。",[16,10836,10837],{},"さらに 3 年が経てば、今の私たちを驚かせる 100 万トークンのコンテキストやエージェントも、このメールと同じように見えるかもしれません。未来が始まったばかりの瞬間に残された、古い品物として。",[10521,10839],{},[81,10841,3185],{"id":3185},[97,10843,10844,10852,10860],{},[100,10845,10846,10851],{},[387,10847,10850],{"href":10848,"rel":10849},"https:\u002F\u002Fopenai.com\u002Findex\u002Fgpt-4-research\u002F",[391],"OpenAI: GPT‑4 research"," — GPT‑4 は 2023 年 3 月に公開され、標準のコンテキストは 8,192 トークン、32,768 トークン版は限定提供でした。",[100,10853,10854,10859],{},[387,10855,10858],{"href":10856,"rel":10857},"https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fmodels",[391],"OpenAI: model catalog"," — 記事公開時点で、最先端の GPT‑5.6 系列には約 105 万トークンのコンテキストと複数カテゴリのツール対応が記載されています。",[100,10861,10862,10867],{},[387,10863,10866],{"href":10864,"rel":10865},"https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Flong-context",[391],"Google AI: Long context"," — 100 万トークン以上のコンテキストを持つ Gemini モデルの用途と制約を説明しています。",{"title":38,"searchDepth":748,"depth":748,"links":10869},[10870,10871,10872,10873,10874,10875],{"id":10675,"depth":276,"text":10676},{"id":10697,"depth":276,"text":10698},{"id":10750,"depth":276,"text":10751},{"id":10794,"depth":276,"text":10795},{"id":10824,"depth":276,"text":10825},{"id":3185,"depth":276,"text":3185},"2023 年に待っていたのは 8K コンテキストの API 利用権でした。3 年後の今、私たちはエージェントに目標、権限、境界、完了条件を伝える方法を学んでいます。",{"src":10878,"title":10879,"description":10880,"duration":10881,"hasAudio":778},"\u002Fexperiences\u002Ffrom-gpt-4-invite-to-agents","GPT-4 の招待からエージェント時代へ · 6 幕のインタラクティブストーリー","コンテキスト、エージェントのワークフロー、能力と責任の変化を 2 分 26 秒で振り返ります。ナレーションは中国語です。",146,{"src":10883,"alt":10884,"caption":10885,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcover-ja-v1.webp","2023 年のメールがガラスのタイムカプセルから現代のエージェント用ワークスペースへとつながる","3 年前はモデルへのアクセスを申請し、今はモデルと協働する方法を学んでいる",{},"\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","2026-07-25",{"title":10640,"description":10876},"blog\u002Ffrom-gpt-4-invite-to-agents",[10892,10893,10894],"AI","GPT-4","エージェント","MMkyyNWTPG5slI4EvXqp_pD8TJrZjZ-_JOgC08cs05o",{"id":10897,"title":10898,"accent":1300,"body":10899,"description":11335,"draft":775,"experience":776,"extension":777,"featured":778,"image":11336,"meta":11340,"navigation":778,"path":11341,"publishedAt":11342,"readingTime":1646,"seo":11343,"stem":11344,"tags":11345,"updatedAt":11347,"video":776,"__hash__":11348,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fedge-stateful-apps.md","エッジでステートフルなアプリケーションを構築する",{"type":13,"value":10900,"toc":11327},[10901,10908,10911,10914,10917,10967,10970,10975,10978,10982,10985,11053,11056,11138,11141,11144,11147,11224,11239,11242,11245,11248,11251,11262,11265,11268,11271,11274,11288,11291,11295,11298,11301,11318,11324],[16,10902,10903,10904,10907],{},"エッジコンピューティングの最も魅力的な約束は、コードをユーザーの近くへ届けることです。しかし、アプリケーションがアカウント、セッション、コメントを保存し始めると、問題は単に「関数をより多くの拠点へデプロイする」ことではなくなります。",[20,10905,10906],{},"状態は実際にどこにあり、誰が責任を持ち、障害時にシステムはどう縮退するのか","を考えなければなりません。",[16,10909,10910],{},"この記事では、ログインとコメント機能を備えたブログを最小の例として、Cloudflare Workers と D1 でステートフルなアプリケーションを構築するときに重要な設計境界を整理します。",[81,10912,10913],{"id":10913},"まず状態の地図を描く",[16,10915,10916],{},"小さく見えるブログにも、少なくとも三種類のまったく異なる状態があります。",[141,10918,10919,10932],{},[144,10920,10921],{},[147,10922,10923,10926,10929],{},[150,10924,10925],{},"状態",[150,10927,10928],{},"保存元",[150,10930,10931],{},"ライフサイクル",[160,10933,10934,10945,10956],{},[147,10935,10936,10939,10942],{},[165,10937,10938],{},"記事本文",[165,10940,10941],{},"Git 内の Markdown",[165,10943,10944],{},"デプロイとともに更新",[147,10946,10947,10950,10953],{},[165,10948,10949],{},"ユーザーとコメント",[165,10951,10952],{},"D1",[165,10954,10955],{},"長期的に永続化",[147,10957,10958,10961,10964],{},[165,10959,10960],{},"ログインセッション",[165,10962,10963],{},"D1 + Cookie",[165,10965,10966],{},"明確な有効期限",[16,10968,10969],{},"記事コンテンツはコードのバージョンと一緒に管理するのが適しています。レビュー、ロールバック、ビルド時検証は必要ですが、読者が実行時に変更する必要はありません。ユーザーデータはその逆で、デプロイのライフサイクルから独立して存在し続けなければなりません。",[1355,10971,10972],{},[16,10973,10974],{},"データベースを選ぶ前に、そのデータがコードのバージョンに従うべきか、ユーザーの行動に従うべきかを判断します。",[16,10976,10977],{},"この区別から、自然にシンプルな構成が導かれます。Nuxt Content が Markdown を、D1 が実行時の業務データを担当し、Worker が一つのリクエストの中で両者を組み合わせます。",[81,10979,10981],{"id":10980},"ネットワークリクエストより-binding-が重要","ネットワークリクエストより binding が重要",[16,10983,10984],{},"Worker が D1 にアクセスするために API Token を保存する必要はなく、実行時に Cloudflare REST API を呼び出すべきでもありません。D1 は binding を通じてランタイムへ直接渡されます。",[33,10986,10988],{"className":3966,"code":10987,"filename":7551,"language":3968,"meta":38,"style":38},"{\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"signal-log-db\",\n      \"database_id\": \"\u003CYOUR_DATABASE_ID>\"\n    }\n  ]\n}\n",[25,10989,10990,10994,11002,11006,11018,11030,11040,11044,11049],{"__ignoreMap":38},[42,10991,10992],{"class":44,"line":45},[42,10993,3975],{"class":1788},[42,10995,10996,10999],{"class":44,"line":276},[42,10997,10998],{"class":52},"  \"d1_databases\"",[42,11000,11001],{"class":1788},": [\n",[42,11003,11004],{"class":44,"line":748},[42,11005,7573],{"class":1788},[42,11007,11008,11011,11013,11016],{"class":44,"line":1590},[42,11009,11010],{"class":52},"      \"binding\"",[42,11012,2071],{"class":1788},[42,11014,11015],{"class":56},"\"DB\"",[42,11017,3988],{"class":1788},[42,11019,11020,11023,11025,11028],{"class":44,"line":1609},[42,11021,11022],{"class":52},"      \"database_name\"",[42,11024,2071],{"class":1788},[42,11026,11027],{"class":56},"\"signal-log-db\"",[42,11029,3988],{"class":1788},[42,11031,11032,11035,11037],{"class":44,"line":1624},[42,11033,11034],{"class":52},"      \"database_id\"",[42,11036,2071],{"class":1788},[42,11038,11039],{"class":56},"\"\u003CYOUR_DATABASE_ID>\"\n",[42,11041,11042],{"class":44,"line":1633},[42,11043,4106],{"class":1788},[42,11045,11046],{"class":44,"line":1646},[42,11047,11048],{"class":1788},"  ]\n",[42,11050,11051],{"class":44,"line":1651},[42,11052,4116],{"class":1788},[16,11054,11055],{},"サーバールートでは、データベースオブジェクトを現在のリクエストコンテキストから取得します。すべての SQL は prepared statement を使います。",[33,11057,11059],{"className":5396,"code":11058,"language":5398,"meta":38,"style":38},"const comment = await db\n  .prepare(`\n    SELECT id, body, created_at\n    FROM comments\n    WHERE post_slug = ?1 AND status = 'published'\n    ORDER BY created_at DESC\n    LIMIT 100\n  `)\n  .bind(postSlug)\n  .all()\n",[25,11060,11061,11075,11087,11092,11097,11102,11107,11112,11119,11129],{"__ignoreMap":38},[42,11062,11063,11065,11068,11070,11072],{"class":44,"line":45},[42,11064,5423],{"class":279},[42,11066,11067],{"class":52}," comment",[42,11069,5429],{"class":279},[42,11071,5545],{"class":279},[42,11073,11074],{"class":1788}," db\n",[42,11076,11077,11079,11082,11084],{"class":44,"line":276},[42,11078,9726],{"class":1788},[42,11080,11081],{"class":48},"prepare",[42,11083,5622],{"class":1788},[42,11085,11086],{"class":56},"`\n",[42,11088,11089],{"class":44,"line":748},[42,11090,11091],{"class":56},"    SELECT id, body, created_at\n",[42,11093,11094],{"class":44,"line":1590},[42,11095,11096],{"class":56},"    FROM comments\n",[42,11098,11099],{"class":44,"line":1609},[42,11100,11101],{"class":56},"    WHERE post_slug = ?1 AND status = 'published'\n",[42,11103,11104],{"class":44,"line":1624},[42,11105,11106],{"class":56},"    ORDER BY created_at DESC\n",[42,11108,11109],{"class":44,"line":1633},[42,11110,11111],{"class":56},"    LIMIT 100\n",[42,11113,11114,11117],{"class":44,"line":1646},[42,11115,11116],{"class":56},"  `",[42,11118,5791],{"class":1788},[42,11120,11121,11123,11126],{"class":44,"line":1651},[42,11122,9726],{"class":1788},[42,11124,11125],{"class":48},"bind",[42,11127,11128],{"class":1788},"(postSlug)\n",[42,11130,11131,11133,11136],{"class":44,"line":1674},[42,11132,9726],{"class":1788},[42,11134,11135],{"class":48},"all",[42,11137,6965],{"class":1788},[16,11139,11140],{},"パラメータ binding は単なるコードスタイルではなく、入力境界の一部です。記事パス、メールアドレス、表示名、コメント本文はいずれもユーザー由来です。どの値も SQL 文字列へ連結してはいけません。",[81,11142,11143],{"id":11143},"セッションには不可逆なダイジェストだけを保存する",[16,11145,11146],{},"ログインに成功すると、ブラウザはランダムなセッショントークンを受け取ります。一方、データベースに保存するのは SHA-256 ダイジェストだけです。セッションテーブルが誤って公開されても、元のトークンをそのままログインに利用することはできません。",[33,11148,11150],{"className":5396,"code":11149,"language":5398,"meta":38,"style":38},"const token = createRandomToken()\nconst tokenHash = await sha256(token)\n\nawait db\n  .prepare('INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)')\n  .bind(tokenHash, userId, expiresAt)\n  .run()\n",[25,11151,11152,11166,11183,11187,11193,11206,11215],{"__ignoreMap":38},[42,11153,11154,11156,11159,11161,11164],{"class":44,"line":45},[42,11155,5423],{"class":279},[42,11157,11158],{"class":52}," token",[42,11160,5429],{"class":279},[42,11162,11163],{"class":48}," createRandomToken",[42,11165,6965],{"class":1788},[42,11167,11168,11170,11173,11175,11177,11180],{"class":44,"line":276},[42,11169,5423],{"class":279},[42,11171,11172],{"class":52}," tokenHash",[42,11174,5429],{"class":279},[42,11176,5545],{"class":279},[42,11178,11179],{"class":48}," sha256",[42,11181,11182],{"class":1788},"(token)\n",[42,11184,11185],{"class":44,"line":748},[42,11186,1587],{"emptyLinePlaceholder":778},[42,11188,11189,11191],{"class":44,"line":1590},[42,11190,5953],{"class":279},[42,11192,11074],{"class":1788},[42,11194,11195,11197,11199,11201,11204],{"class":44,"line":1609},[42,11196,9726],{"class":1788},[42,11198,11081],{"class":48},[42,11200,5622],{"class":1788},[42,11202,11203],{"class":56},"'INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)'",[42,11205,5791],{"class":1788},[42,11207,11208,11210,11212],{"class":44,"line":1624},[42,11209,9726],{"class":1788},[42,11211,11125],{"class":48},[42,11213,11214],{"class":1788},"(tokenHash, userId, expiresAt)\n",[42,11216,11217,11219,11222],{"class":44,"line":1633},[42,11218,9726],{"class":1788},[42,11220,11221],{"class":48},"run",[42,11223,6965],{"class":1788},[16,11225,11226,11227,11230,11231,11234,11235,11238],{},"Cookie には ",[25,11228,11229],{},"HttpOnly"," を設定してフロントエンド JavaScript から読めないようにし、",[25,11232,11233],{},"SameSite=Lax"," でクロスサイトリクエストのリスクを下げ、本番環境ではさらに ",[25,11236,11237],{},"Secure"," を付けます。",[16,11240,11241],{},"これでシステムが「絶対に安全」になるわけではありません。より正確には、各レイヤーが仕事に必要な最小限の能力だけを持つようにしています。",[81,11243,11244],{"id":11244},"一貫性はユーザー操作を中心に設計する",[16,11246,11247],{},"D1 はリレーショナルデータベースですが、エッジアプリケーションでも、どのフローが強い一貫性を必要とするかを明確にする必要があります。",[16,11249,11250],{},"ブログの重要な操作は多くありません。",[408,11252,11253,11256,11259],{},[100,11254,11255],{},"登録直後にログインできること。",[100,11257,11258],{},"コメント送信の成功直後に現在のページへ表示されること。",[100,11260,11261],{},"ログアウト直後に古いセッションが無効になること。",[16,11263,11264],{},"これらのフローは、同じプライマリデータベースに書き込み、その直後の読み取りもそこから行うべきです。記事一覧や公開コメント一覧などの読み取り専用の場面にこそ、より大きなキャッシュ余地があります。",[16,11266,11267],{},"「どこをキャッシュできるか」ではなく、「ユーザーが今完了した約束は何か」から考えます。",[81,11269,11270],{"id":11270},"障害にもプロダクト設計が必要",[16,11272,11273],{},"エッジランタイムは失敗することがあり、データベースも一時的に利用できなくなることがあります。違いは、その失敗をユーザーが理解できる状態に変えられるかどうかです。",[97,11275,11276,11279,11282,11285],{},[100,11277,11278],{},"ログイン失敗時は「メールアドレスが存在しない」と「パスワードが違う」を区別せず、アカウント列挙を防ぎます。",[100,11280,11281],{},"コメント書き込みに失敗したら入力内容を保持し、再試行できるようにします。",[100,11283,11284],{},"コンテンツ取得に失敗したら、空白ページではなく正しいエラー状態を返します。",[100,11286,11287],{},"サーバーエラーは構造化ログに記録しますが、内部 SQL やスタックトレースをブラウザへ返しません。",[16,11289,11290],{},"信頼性とは「絶対に失敗しない」ことではなく、失敗が起きても境界が壊れないことです。",[81,11292,11294],{"id":11293},"最後に退屈なアーキテクチャを保つ","最後に、退屈なアーキテクチャを保つ",[16,11296,11297],{},"この構成にはメッセージキューも、分散ロックも、追加の認証サービスもありません。それらに価値がないからではなく、現在の問題がまだ必要としていないからです。",[16,11299,11300],{},"長期的に保守しやすい個人サイトの基礎は、たいてい次のようにシンプルです。",[97,11302,11303,11306,11309,11312,11315],{},[100,11304,11305],{},"コンテンツは Git とともに管理する。",[100,11307,11308],{},"実行時データは D1 に保存する。",[100,11310,11311],{},"シークレットは Worker secret に置く。",[100,11313,11314],{},"入力は境界で検証する。",[100,11316,11317],{},"障害を明確にし、観測可能かつ復旧可能にする。",[16,11319,11320,11321],{},"トラフィックやプロダクト要件が本当に変化したときに、証拠に合わせてアーキテクチャを進化させれば十分です。",[20,11322,11323],{},"エッジはすでに十分高速です。エンジニアリングの仕事は、その単純さを保つことです。",[744,11325,11326],{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":38,"searchDepth":748,"depth":748,"links":11328},[11329,11330,11331,11332,11333,11334],{"id":10913,"depth":276,"text":10913},{"id":10980,"depth":276,"text":10981},{"id":11143,"depth":276,"text":11143},{"id":11244,"depth":276,"text":11244},{"id":11270,"depth":276,"text":11270},{"id":11293,"depth":276,"text":11294},"Cloudflare Workers は高速で D1 は近くにあります。しかし本当に難しいのは、状態、一貫性、障害の境界を明確に設計することです。",{"src":11337,"alt":11338,"caption":11339,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Fedge-stateful-apps\u002Fcover-v1.webp","中央のデータベースを囲むエッジノードがステートフルなアプリケーショントポロジーを構成している","状態、エッジノード、一貫性の境界",{},"\u002Fblog\u002Fedge-stateful-apps","2026-07-21",{"title":10898,"description":11335},"blog\u002Fedge-stateful-apps",[2654,10952,11346],"エンジニアリング","2026-07-24","45JW9j2ScEo7kmmOD41SBwFEzTUp3ok22gVd9dOmZ9k",{"id":11350,"title":11351,"accent":11,"body":11352,"description":11737,"draft":775,"experience":776,"extension":777,"featured":775,"image":11738,"meta":11742,"navigation":778,"path":11743,"publishedAt":11744,"readingTime":1624,"seo":11745,"stem":11746,"tags":11747,"updatedAt":776,"video":776,"__hash__":11750,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Nuxt 4 におけるアーキテクチャの引き算",{"type":13,"value":11353,"toc":11729},[11354,11357,11364,11368,11371,11422,11425,11432,11435,11438,11562,11565,11583,11586,11589,11592,11595,11598,11603,11607,11614,11620,11623,11626,11629,11632,11697,11700,11703,11706,11723,11726],[16,11355,11356],{},"Nuxt プロジェクトが複雑になる理由は、ビジネスが本当に複雑だからとは限りません。あらゆる将来の可能性を、あまりに早くアーキテクチャへ入れてしまうことがよくあります。",[16,11358,11359,11360,11363],{},"個人ブログは良い調整基準になります。ページ数は限られ、コンテンツ構造は安定し、インタラクションは少数の領域に集中しています。フレームワークの最も価値ある役割は、機能を増やすことではなく、",[20,11361,11362],{},"不要な選択肢を削除すること","だと気づかせてくれます。",[81,11365,11367],{"id":11366},"デフォルトの-ssr-から始める","デフォルトの SSR から始める",[16,11369,11370],{},"技術記事は検索エンジンに理解される必要があり、低速な回線でも本文をできるだけ早く表示したいものです。Nuxt のデフォルトであるサーバーサイドレンダリングは、その目的に合っています。",[33,11372,11375],{"className":5396,"code":11373,"filename":11374,"language":5398,"meta":38,"style":38},"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts",[25,11376,11377,11388,11399,11404,11414,11418],{"__ignoreMap":38},[42,11378,11379,11381,11383,11386],{"class":44,"line":45},[42,11380,5211],{"class":279},[42,11382,6909],{"class":279},[42,11384,11385],{"class":48}," defineNuxtConfig",[42,11387,5438],{"class":1788},[42,11389,11390,11393,11396],{"class":44,"line":276},[42,11391,11392],{"class":1788},"  modules: [",[42,11394,11395],{"class":56},"'@nuxt\u002Fcontent'",[42,11397,11398],{"class":1788},"],\n",[42,11400,11401],{"class":44,"line":748},[42,11402,11403],{"class":1788},"  nitro: {\n",[42,11405,11406,11409,11412],{"class":44,"line":1590},[42,11407,11408],{"class":1788},"    preset: ",[42,11410,11411],{"class":56},"'cloudflare'",[42,11413,3988],{"class":1788},[42,11415,11416],{"class":44,"line":1609},[42,11417,5495],{"class":1788},[42,11419,11420],{"class":44,"line":1624},[42,11421,5529],{"class":1788},[16,11423,11424],{},"サイト全体を SPA に変える必要も、ページごとに別のデータ取得方法を発明する必要もありません。最初のクエリとレンダリングはサーバーで完了し、クライアントが引き継いだ後も同じルーターを使います。",[16,11426,11427,11428,11431],{},"ブラウザ API に依存する小さな領域だけ、局所的に ",[25,11429,11430],{},"ClientOnly"," を使えば十分です。一つのクライアントコンポーネントのために、ページ全体のセマンティクスとファーストビューを犠牲にしてはいけません。",[81,11433,11434],{"id":11434},"コンテンツクエリをページ境界に置く",[16,11436,11437],{},"ホーム、記事一覧、記事詳細では必要なデータ粒度が異なります。クエリは利用するページの近くに置きます。",[33,11439,11441],{"className":5396,"code":11440,"language":5398,"meta":38,"style":38},"const { data: posts } = await useAsyncData('latest-posts', () => {\n  return queryCollection('posts')\n    .where('draft', '=', false)\n    .order('publishedAt', 'DESC')\n    .limit(6)\n    .all()\n})\n",[25,11442,11443,11478,11492,11517,11536,11550,11558],{"__ignoreMap":38},[42,11444,11445,11447,11449,11452,11454,11457,11459,11461,11463,11466,11468,11471,11474,11476],{"class":44,"line":45},[42,11446,5423],{"class":279},[42,11448,7985],{"class":1788},[42,11450,11451],{"class":5734},"data",[42,11453,2071],{"class":1788},[42,11455,11456],{"class":52},"posts",[42,11458,7990],{"class":1788},[42,11460,1832],{"class":279},[42,11462,5545],{"class":279},[42,11464,11465],{"class":48}," useAsyncData",[42,11467,5622],{"class":1788},[42,11469,11470],{"class":56},"'latest-posts'",[42,11472,11473],{"class":1788},", () ",[42,11475,7975],{"class":279},[42,11477,5930],{"class":1788},[42,11479,11480,11482,11485,11487,11490],{"class":44,"line":276},[42,11481,8163],{"class":279},[42,11483,11484],{"class":48}," queryCollection",[42,11486,5622],{"class":1788},[42,11488,11489],{"class":56},"'posts'",[42,11491,5791],{"class":1788},[42,11493,11494,11497,11500,11502,11505,11507,11510,11512,11515],{"class":44,"line":748},[42,11495,11496],{"class":1788},"    .",[42,11498,11499],{"class":48},"where",[42,11501,5622],{"class":1788},[42,11503,11504],{"class":56},"'draft'",[42,11506,5743],{"class":1788},[42,11508,11509],{"class":56},"'='",[42,11511,5743],{"class":1788},[42,11513,11514],{"class":52},"false",[42,11516,5791],{"class":1788},[42,11518,11519,11521,11524,11526,11529,11531,11534],{"class":44,"line":1590},[42,11520,11496],{"class":1788},[42,11522,11523],{"class":48},"order",[42,11525,5622],{"class":1788},[42,11527,11528],{"class":56},"'publishedAt'",[42,11530,5743],{"class":1788},[42,11532,11533],{"class":56},"'DESC'",[42,11535,5791],{"class":1788},[42,11537,11538,11540,11543,11545,11548],{"class":44,"line":1609},[42,11539,11496],{"class":1788},[42,11541,11542],{"class":48},"limit",[42,11544,5622],{"class":1788},[42,11546,11547],{"class":52},"6",[42,11549,5791],{"class":1788},[42,11551,11552,11554,11556],{"class":44,"line":1624},[42,11553,11496],{"class":1788},[42,11555,11135],{"class":48},[42,11557,6965],{"class":1788},[42,11559,11560],{"class":44,"line":1633},[42,11561,5529],{"class":1788},[16,11563,11564],{},"この方法には直接的な利点があります。",[97,11566,11567,11574,11577,11580],{},[100,11568,11569,11570,11573],{},"データが SSR に参加し、",[25,11571,11572],{},"onMounted"," 後に点滅しながら表示されません。",[100,11575,11576],{},"クエリキーが明確なので、Nuxt は hydration 時に payload を再利用できます。",[100,11578,11579],{},"ページ自身が必要なものを理解し、汎用 store を経由する必要がありません。",[100,11581,11582],{},"下書きの除外と並び順が、本当に関係する場所に現れます。",[16,11584,11585],{},"複数のページが同じ複雑な業務状態を共有するようになったときにだけ、composable や store へ抽出する価値があります。",[81,11587,11588],{"id":11588},"安定して繰り返すものだけをコンポーネント化する",[16,11590,11591],{},"「再利用できそう」はコンポーネント境界ではありません。本当に安定した繰り返しには、通常、構造、意味、動作が同時に含まれます。",[16,11593,11594],{},"このブログでは記事カードが適切なコンポーネントです。ホームと一覧ページの両方に現れ、メタデータ、タイトル、概要、タグ、遷移動作が固定されています。",[16,11596,11597],{},"一方、ホームの Hero は大きくても、必ずしも五つの小さなコンポーネントへ分割する必要はありません。一つのページにしか現れず、内部要素がまとまって一つの視覚的な物語を作ります。早すぎる分割は、スタイルの関係を複数ファイルへ散らすだけです。",[1355,11599,11600],{},[16,11601,11602],{},"コンポーネントはファイル整理の道具ではなく、独立して理解し再利用できる UI 契約です。",[81,11604,11606],{"id":11605},"サーバー-api-のインターフェースを狭く保つ","サーバー API のインターフェースを狭く保つ",[16,11608,11609,11610,11613],{},"ログインとコメントは実行時データなので ",[25,11611,11612],{},"server\u002Fapi"," に置きます。各 endpoint は一つの仕事だけを行います。",[33,11615,11618],{"className":11616,"code":11617,"language":233,"meta":38},[231],"POST \u002Fapi\u002Fauth\u002Fregister\nPOST \u002Fapi\u002Fauth\u002Flogin\nPOST \u002Fapi\u002Fauth\u002Flogout\nGET  \u002Fapi\u002Fauth\u002Fsession\nGET  \u002Fapi\u002Fcomments\u002F:post\nPOST \u002Fapi\u002Fcomments\u002F:post\n",[25,11619,11617],{"__ignoreMap":38},[16,11621,11622],{},"狭いインターフェースは認可規則を確認しやすくします。コメントの読み取りは公開、作成にはセッションが必要、ログアウトはユーザープロフィールに触れず現在のトークンだけを破棄します。",[16,11624,11625],{},"一つの endpoint が読み取り、書き込み、ログイン、形式変換を同時に扱うなら、すでに明確な境界を失っています。",[81,11627,11628],{"id":11628},"状態を正しい場所へ戻す",[16,11630,11631],{},"Vue のリアクティブな状態は便利ですが、すべての状態を JavaScript に置くべきではありません。",[141,11633,11634,11643],{},[144,11635,11636],{},[147,11637,11638,11640],{},[150,11639,10925],{},[150,11641,11642],{},"最適な場所",[160,11644,11645,11653,11663,11671,11682,11689],{},[147,11646,11647,11650],{},[165,11648,11649],{},"現在の記事",[165,11651,11652],{},"ルート",[147,11654,11655,11658],{},[165,11656,11657],{},"検索語",[165,11659,11660,11661],{},"ページの ",[25,11662,8946],{},[147,11664,11665,11668],{},[165,11666,11667],{},"タグ絞り込み",[165,11669,11670],{},"URL query + ページ状態",[147,11672,11673,11676],{},[165,11674,11675],{},"ログインユーザー",[165,11677,11678,11679],{},"SSR 安全な ",[25,11680,11681],{},"useState",[147,11683,11684,11686],{},[165,11685,10938],{},[165,11687,11688],{},"Nuxt Content",[147,11690,11691,11694],{},[165,11692,11693],{},"セッションの真正性",[165,11695,11696],{},"サーバー側の D1",[16,11698,11699],{},"状態を正しい場所へ置けば、大量の「同期ロジック」が自然に消えます。ルートを監視して store を修復する必要も、localStorage でログイン状態が信頼できるふりをする必要もなくなります。",[81,11701,11702],{"id":11702},"引き算は機能を減らすことではない",[16,11704,11705],{},"良いアーキテクチャの引き算は、ユーザーから何も奪いません。むしろ、ユーザーが実際に感じる部分へ複雑さの予算を残します。",[97,11707,11708,11711,11714,11717,11720],{},[100,11709,11710],{},"より明確なタイポグラフィ。",[100,11712,11713],{},"より速いファーストビュー。",[100,11715,11716],{},"より信頼できるログイン。",[100,11718,11719],{},"より快適なモバイル読書。",[100,11721,11722],{},"続けやすい Markdown 執筆フロー。",[16,11724,11725],{},"Nuxt 4 には十分な機能があります。成熟したプロジェクトとは、それらをすべて使うものではなく、今は使わなくてよい機能を知っているものです。",[744,11727,11728],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":38,"searchDepth":748,"depth":748,"links":11730},[11731,11732,11733,11734,11735,11736],{"id":11366,"depth":276,"text":11367},{"id":11434,"depth":276,"text":11434},{"id":11588,"depth":276,"text":11588},{"id":11605,"depth":276,"text":11606},{"id":11628,"depth":276,"text":11628},{"id":11702,"depth":276,"text":11702},"コンテンツ中心のサイトに複雑なフロントエンド状態は不要です。ルーティング、SSR、コンテンツクエリを、それぞれ得意な場所へ戻します。",{"src":11739,"alt":11740,"caption":11741,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","複雑なモジュールが明確でコンパクトな Nuxt アプリケーションの中心へ収束していく","アーキテクチャの引き算で境界を明確にする",{},"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":11351,"description":11737},"blog\u002Fnuxt-4-architecture-subtraction",[11748,10634,11749],"Nuxt","アーキテクチャ","ssY4mnEKHetrOcAiq0xs3OxgnqvkR3kHHmApyaxbxhc",{"id":11752,"title":11753,"accent":3323,"body":11754,"description":12101,"draft":775,"experience":776,"extension":777,"featured":775,"image":12102,"meta":12106,"navigation":778,"path":12107,"publishedAt":12108,"readingTime":1633,"seo":12109,"stem":12110,"tags":12111,"updatedAt":776,"video":776,"__hash__":12113,"contentLocale":6,"isFallback":775},"posts_ja\u002Fblog\u002Fobservable-ai-systems.md","AI システムの可観測性は Token の請求書だけではない",{"type":13,"value":11755,"toc":12088},[11756,11759,11764,11767,11770,11773,11777,11780,11784,11787,11791,11794,11798,11801,11804,11808,11815,11928,11931,11934,11951,11954,11957,11960,12014,12017,12020,12023,12040,12043,12046,12049,12052,12066,12069,12073,12076,12082,12085],[16,11757,11758],{},"従来のサービスで問題が起きたとき、私たちはステータスコード、レイテンシ、エラースタックを確認します。AI アプリケーションでもこれらの指標は重要ですが、最も大切な問いに答えるには不十分なことがよくあります。",[16,11760,11761],{},[20,11762,11763],{},"モデルはなぜこの選択をしたのか。",[16,11765,11766],{},"答えは一つのログにはありません。ユーザーの目標、コンテキストの組み立て、モデルの判断、ツールの実行、最終結果までのチェーン全体にあります。",[81,11768,11769],{"id":11769},"まず四種類の失敗を区別する",[16,11771,11772],{},"すべての失敗を「モデルの性能が悪い」と記録してしまうと、チームは具体的な行動を取れません。少なくとも次の四つを区別する必要があります。",[248,11774,11776],{"id":11775},"_1-入力の失敗","1. 入力の失敗",[16,11778,11779],{},"ユーザーの目標が不完全、またはタスクの完了に必要なコンテキストをシステムが取得できていません。この場合、モデルをアップグレードしても通常は解決しません。",[248,11781,11783],{"id":11782},"_2-推論の失敗","2. 推論の失敗",[16,11785,11786],{},"コンテキストは正しいものの、モデルが誤った経路を選び、制約を見落とし、あるいは早すぎる結論に到達しています。",[248,11788,11790],{"id":11789},"_3-ツールの失敗","3. ツールの失敗",[16,11792,11793],{},"モデルの選択は正しいものの、権限、ネットワーク、パラメータ、外部状態などの理由で実行に失敗しています。",[248,11795,11797],{"id":11796},"_4-伝達の失敗","4. 伝達の失敗",[16,11799,11800],{},"内部の作業は完了しているのに、最終回答が結果、制約、次のステップを明確に説明できていません。",[16,11802,11803],{},"これら四つの問題は、プロダクト、プロンプト設計、プラットフォームエンジニアリング、インタラクション設計という異なる担当者を必要とします。可観測性の最初の価値は、問題を正しい人へ戻すことです。",[81,11805,11807],{"id":11806},"一つの実行には共通の-trace-が必要","一つの実行には共通の Trace が必要",[16,11809,11810,11811,11814],{},"各ユーザーリクエストには安定した ",[25,11812,11813],{},"trace_id"," を割り当て、モデル呼び出しとツール実行をつなぎます。",[33,11816,11818],{"className":5396,"code":11817,"language":5398,"meta":38,"style":38},"type AgentEvent = {\n  traceId: string\n  spanId: string\n  parentSpanId?: string\n  type: 'model' | 'tool' | 'handoff' | 'result'\n  startedAt: string\n  durationMs?: number\n  status: 'running' | 'ok' | 'error'\n}\n",[25,11819,11820,11832,11841,11850,11860,11885,11894,11904,11924],{"__ignoreMap":38},[42,11821,11822,11825,11828,11830],{"class":44,"line":45},[42,11823,11824],{"class":279},"type",[42,11826,11827],{"class":48}," AgentEvent",[42,11829,5429],{"class":279},[42,11831,5930],{"class":1788},[42,11833,11834,11837,11839],{"class":44,"line":276},[42,11835,11836],{"class":5734},"  traceId",[42,11838,5737],{"class":279},[42,11840,6894],{"class":52},[42,11842,11843,11846,11848],{"class":44,"line":748},[42,11844,11845],{"class":5734},"  spanId",[42,11847,5737],{"class":279},[42,11849,6894],{"class":52},[42,11851,11852,11855,11858],{"class":44,"line":1590},[42,11853,11854],{"class":5734},"  parentSpanId",[42,11856,11857],{"class":279},"?:",[42,11859,6894],{"class":52},[42,11861,11862,11865,11867,11870,11872,11875,11877,11880,11882],{"class":44,"line":1609},[42,11863,11864],{"class":5734},"  type",[42,11866,5737],{"class":279},[42,11868,11869],{"class":56}," 'model'",[42,11871,311],{"class":279},[42,11873,11874],{"class":56}," 'tool'",[42,11876,311],{"class":279},[42,11878,11879],{"class":56}," 'handoff'",[42,11881,311],{"class":279},[42,11883,11884],{"class":56}," 'result'\n",[42,11886,11887,11890,11892],{"class":44,"line":1624},[42,11888,11889],{"class":5734},"  startedAt",[42,11891,5737],{"class":279},[42,11893,6894],{"class":52},[42,11895,11896,11899,11901],{"class":44,"line":1633},[42,11897,11898],{"class":5734},"  durationMs",[42,11900,11857],{"class":279},[42,11902,11903],{"class":52}," number\n",[42,11905,11906,11909,11911,11914,11916,11919,11921],{"class":44,"line":1646},[42,11907,11908],{"class":5734},"  status",[42,11910,5737],{"class":279},[42,11912,11913],{"class":56}," 'running'",[42,11915,311],{"class":279},[42,11917,11918],{"class":56}," 'ok'",[42,11920,311],{"class":279},[42,11922,11923],{"class":56}," 'error'\n",[42,11925,11926],{"class":44,"line":1651},[42,11927,4116],{"class":1788},[16,11929,11930],{},"ツール呼び出しは、モデル判断の外部表現です。呼び出した理由を残さず戻り値だけを記録すると判断コンテキストを失います。プロンプトだけを保存してツール結果を残さなければ、実行が意図どおりだったか判断できません。",[16,11932,11933],{},"観測は無制限な収集を意味しません。生のプロンプトには個人情報、認証情報、業務データが含まれる可能性があります。より安全な方法は次のとおりです。",[97,11935,11936,11939,11942,11945,11948],{},[100,11937,11938],{},"デフォルトでは構造化メタデータを記録する。",[100,11940,11941],{},"本文はフィールド単位でマスキングする。",[100,11943,11944],{},"デバッグ用サンプルには短い保存期間を設定する。",[100,11946,11947],{},"ユーザーや事業者が学習・分析への不参加を明示的に選べるようにする。",[100,11949,11950],{},"ログにシークレットや完全な個人識別情報を保存しない。",[81,11952,11953],{"id":11953},"品質指標をタスクに近づける",[16,11955,11956],{},"Token、レイテンシ、モデルエラー率はプラットフォーム指標ですが、ユーザーが結果を得られたかは示せません。",[16,11958,11959],{},"コード修正 Agent にとって、より意味のある指標は次のようなものです。",[141,11961,11962,11972],{},[144,11963,11964],{},[147,11965,11966,11969],{},[150,11967,11968],{},"レイヤー",[150,11970,11971],{},"指標の例",[160,11973,11974,11982,11990,11998,12006],{},[147,11975,11976,11979],{},[165,11977,11978],{},"結果",[165,11980,11981],{},"影響を受けるテストが通ったか",[147,11983,11984,11987],{},[165,11985,11986],{},"プロセス",[165,11988,11989],{},"関連する制約ファイルを読んだか",[147,11991,11992,11995],{},[165,11993,11994],{},"安全性",[165,11996,11997],{},"タスク範囲外のファイルを変更していないか",[147,11999,12000,12003],{},[165,12001,12002],{},"効率",[165,12004,12005],{},"有効なツール呼び出し \u002F 全ツール呼び出し",[147,12007,12008,12011],{},[165,12009,12010],{},"伝達",[165,12012,12013],{},"検証可能なファイルやコミットを提供したか",[16,12015,12016],{},"単なる呼び出し回数より「有効なツール呼び出し」が重要です。一度の正確な検索が、十回の推測的な読み取りを置き換えることがあります。",[81,12018,12019],{"id":12019},"評価を日常的なフィードバックへ変える",[16,12021,12022],{},"オフライン benchmark はモデルのバージョン比較には役立ちますが、実際のワークフローすべてを網羅できません。持続可能な評価セットは、本番環境の価値あるサンプルから作ります。",[408,12024,12025,12028,12031,12034,12037],{},[100,12026,12027],{},"明確に成功した実行と明確に失敗した実行を集める。",[100,12029,12030],{},"判断構造を保ちながら個人情報を削除する。",[100,12032,12033],{},"総合点だけでなく、失敗の種類をラベル付けする。",[100,12035,12036],{},"プロンプト、モデル、ツールを変更したときに再実行する。",[100,12038,12039],{},"結果品質、コスト、レイテンシを同時に比較する。",[16,12041,12042],{},"評価セットは最初から大規模である必要はありません。境界が明確で安定して再現できる二十件のケースは、出所が曖昧な二千件のサンプルよりも、エンジニアリングを導くことがあります。",[81,12044,12045],{"id":12045},"人のための可観測性",[16,12047,12048],{},"最後の観測レイヤーはダッシュボードではなく、プロダクトの画面にあります。",[16,12050,12051],{},"Agent がファイルを読み、承認を待ち、デプロイを実行しているとき、ユーザーはどの段階にいるのかを知る必要があります。良いステータス更新は内部思考を垂れ流すのではなく、次を伝えます。",[97,12053,12054,12057,12060,12063],{},[100,12055,12056],{},"現在何を完了しようとしているか。",[100,12058,12059],{},"すでに何を確認したか。",[100,12061,12062],{},"ユーザーの行動が必要か。",[100,12064,12065],{},"どのリスクがまだ残っているか。",[16,12067,12068],{},"この可視性は、システムの信頼性に対するユーザーの判断を直接変えます。数十秒かかっても過程が明確なタスクは、十秒後に突然不透明な結果を返すタスクより安心できることがあります。",[81,12070,12072],{"id":12071},"説明可能であることはすべてを公開することではない","説明可能であることは、すべてを公開することではない",[16,12074,12075],{},"AI システムは内部のすべての token をユーザーへ見せる必要はなく、機密コンテキストをそのままログに書くべきでもありません。必要なのは、十分に明確な因果チェーンです。",[33,12077,12080],{"className":12078,"code":12079,"language":233,"meta":38},[231],"ユーザーの目標\n  → どのコンテキストを使ったか\n  → どのツールを選んだか\n  → ツールがどの状態を返したか\n  → 最終的にどの結果を届けたか\n",[25,12081,12079],{"__ignoreMap":38},[16,12083,12084],{},"このチェーンを検索、比較、再現できるようになって初めて、チームは「時々とても賢い」を「長期的に信頼できる」へ変えられます。",[744,12086,12087],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":38,"searchDepth":748,"depth":748,"links":12089},[12090,12096,12097,12098,12099,12100],{"id":11769,"depth":276,"text":11769,"children":12091},[12092,12093,12094,12095],{"id":11775,"depth":748,"text":11776},{"id":11782,"depth":748,"text":11783},{"id":11789,"depth":748,"text":11790},{"id":11796,"depth":748,"text":11797},{"id":11806,"depth":276,"text":11807},{"id":11953,"depth":276,"text":11953},{"id":12019,"depth":276,"text":12019},{"id":12045,"depth":276,"text":12045},{"id":12071,"depth":276,"text":12072},"プロンプト、ツール呼び出し、ユーザーに見える結果まで、AI アプリケーションには「なぜ」を説明できる観測チェーンが必要です。",{"src":12103,"alt":12104,"caption":12105,"width":8913,"height":8914},"\u002Fmedia\u002Fposts\u002Fobservable-ai-systems\u002Fcover-v1.webp","AI リクエストが入力からモデルとツールのノードを通り、完全な観測チェーンを形成している","プロンプトから結果までを説明できる Trace",{},"\u002Fblog\u002Fobservable-ai-systems","2026-07-09",{"title":11753,"description":12101},"blog\u002Fobservable-ai-systems",[10892,12112,11346],"可観測性","t0LYdTk81d8m4r5OxPnfA9rpTh4e3hfr1sF49X_xm5o",1787735716955]