FastComments.com

Val Town アプリにライブコメントを追加

Val Town は Deno 上で TypeScript を実行するため、val は実際のサーバーです。これにより FastComments に適しています:ウィジェットはページ上の script タグで、Secure SSO や webhook の検証など、シークレットが必要な処理は同じ val 内でサーバーサイドとして実行できます。

このガイドでは、HTTP val にコメントウィジェットを追加し、インデックスページにコメント数を表示し、既に持っている Val Town アカウントでユーザーをサインインさせ、コメント webhook を受信する方法を説明します。

試すためにアカウントは必要ありません。例では tenantId: "demo" を使用した共有サンドボックスを利用し、ステップ 2 で自分のものに切り替える方法を説明します。

インデックスページのコメント数 Internal Link

インデックスページでは、行ごとにコメントカウントウィジェットを1つずつレンダリングしないでください。これは投稿ごとに1リクエストになります。ページ全体で1回のリクエストで済む一括カウントを使用してください。

urlId(スレッドが使用する)で各行にマークし、まとめてウィジェットを1回だけロードします:

インデックスページの一括コメント数
Copy CopyRun External Link
1
2<ul>
3 {posts.map((post) => (
4 <li>
5 <a href={post.slug}>{post.title}</a>{" "}
6 <span class="fast-comments-count" data-fast-comments-url-id={post.slug}></span>
7 </li>
8 ))}
9</ul>
10
11<script
12 dangerouslySetInnerHTML={{
13 __html: `window.FastCommentsBulkCountConfig = ${
14 JSON.stringify({ tenantId: TENANT_ID })
15 };`,
16 }}
17/>
18<script src="https://cdn.fastcomments.com/js/embed-widget-comment-count-bulk.min.js"></script>
19

このスクリプトはページ上のすべての .fast-comments-count 要素を検出し、そのカウントを埋め込みます。

data-fast-comments-url-id は、投稿のコメントウィジェットが使用する urlId と一致する必要があります。ウィジェットがスラッグを使用する場合、マーカーもスラッグを使用します。不一致の場合、コメントがあるスレッドでも0が表示されます。

スクリプトは window.FastCommentsBulkCountConfig をポーリングするため、設定をスクリプトタグの前に置くか後に置くかは関係ありません。

std/oauth による安全なシングルサインオン Internal Link

すでに訪問者が誰であるかを val が把握している場合、Secure SSO はその ID をウィジェットに渡すため、訪問者は2回目のログインを目にすることはありません。構築すべきエンドポイントも、実行時に呼び出すべき API もありません。3つの値をサーバー側で計算し、ウィジェットの設定に渡すだけです。

Val Town は std/oauth による設定不要のログインを提供しているため、訪問者は既に持っている Val Town アカウントでサインインできます。これをお使いのアプリで使用している認証方式に差し替えても、FastComments 側は変更不要です。

サーバーでペイロードを構築する

API シークレットはペイロードに署名するためのもので、ブラウザーコードに到達してはいけません。npm から SDK をインストールしてください。Val Town の Deno ランタイムでそのまま動作します:

sso.ts
Copy CopyRun External Link
1
2import { SecureSSOPayloadBuilder } from "npm:fastcomments-sdk/server";
3
4export function buildSSOPayload(user) {
5 // id must be stable for the same person, or they get a new comment identity on every login.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email is required and must be unique.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // username is required and cannot be an email.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() は { userDataJSONBase64, verificationHash, timestamp } を返します。ブラウザーに到達するのはこの3つの値だけです。シークレットはこれらに署名した後で破棄されるため、ページ内のどの情報を使っても別のユーザーを偽装することはできません。

ウィジェットに渡す

Widget config with SSO
Copy CopyRun External Link
1
2import { getOAuthUserData, oauthMiddleware } from "https://esm.town/v/std/oauth/middleware.ts";
3
4app.get("/", async (c) => {
5 const session = await getOAuthUserData(c.req.raw);
6 const user = session?.user;
7
8 const config = {
9 tenantId: TENANT_ID,
10 urlId: "my-thread",
11 ...(user
12 ? { sso: { ...buildSSOPayload(user), logoutURL: "/logout" } }
13 : { sso: { loginURL: "/auth/login" } }),
14 };
15
16 // ...render the widget with this config
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware は GET /auth/login、GET /auth/callback、POST /auth/logout を自動的に追加します。なお、ログアウトは POST ですが、ウィジェットは logoutURL に GET で遷移するため、logoutURL には POST を送信する小さな独自ルートを指定してください。

訪問者がログアウトしている場合は、loginURL のみを含む sso を渡します。するとウィジェットは匿名コメントボックスの代わりにログインプロンプトを表示します。

よくある問題

timestamp はエポックミリ秒であり、未来の時刻であってはならず、2日以上古くてもいけません。ハッシュを計算するのと同じリクエスト内でサーバー側で生成してください。ブラウザー側で生成するのが典型的な失敗パターンです。その場合、値がハッシュ化されたものと異なり、すべてのコメントが拒否されます。

ID プロバイダーから isAdmin や isModerator を設定してはいけません。Val Town アカウントでのサインインは、誰があなたのサイトをモデレートすべきかを示すものではありません。

フィールドの一覧、グループ制限付きスレッド、バッジの詳細については、SSO ガイド を参照してください。

Webhook の受信 Internal Link

A val は自然な Webhook 受信者です。安定した URL を持ち、署名を検証でき、SQLite と BLOB ストレージが組み込まれています。

FastComments は ${timestamp}.${body} にアカウントの API シークレットで署名し、2 つのヘッダーを送信します。

Webhook ヘッダー
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

このメソッドはイベントを伝えます。作成または更新されたコメントには PUT、削除されたコメントには DELETE を使用します。

配信の検証
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // The exact bytes that arrived. Do NOT use c.req.json() and re-serialize.
6 const rawBody = await c.req.raw.text();
7 const timestamp = c.req.raw.headers.get("X-FastComments-Timestamp");
8 const signature = c.req.raw.headers.get("X-FastComments-Signature");
9
10 if (!timestamp || !signature) return new Response("Missing headers", { status: 400 });
11
12 // Reject stale deliveries so a captured request cannot be replayed later.
13 if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) {
14 return new Response("Timestamp outside window", { status: 400 });
15 }
16
17 const expected = "sha256=" + createHmac("sha256", Deno.env.get("FASTCOMMENTS_API_SECRET"))
18 .update(`${timestamp}.${rawBody}`)
19 .digest("hex");
20
21 const a = new TextEncoder().encode(signature);
22 const b = new TextEncoder().encode(expected);
23 if (a.length !== b.length || !timingSafeEqual(a, b)) {
24 return new Response("Signature mismatch", { status: 401 });
25 }
26
27 // ...handle JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

問題となる2つの点

生バイトを検証する。 JSON を解析して再シリアライズするとキーの順序や空白が変わり、ハッシュが一致せず、明確な原因なしにすべての配信が失敗します。これが Webhook 受信者が「うまく動かない」一般的な理由です。

一定時間で比較する。 署名に対して単純な === を使用すると、何バイト一致したかが漏洩し、1 バイトずつ偽造するのに十分です。

イベントの処理

迅速に応答してください。FastComments は 2xx 以外のステータスで再試行し、失敗が続くエンドポイントは最終的に自動で無効化されます。そのため、インラインで処理せずにレスポンスを返した後に実際の作業を行ってください。

コメント ID に対して冪等に動作させてください。再試行時には新しいタイムスタンプで再署名され、編集や削除時に同じコメント ID が再度届くため、重複排除できる安定した情報はありません。

サンプル Vals Internal Link

リミックスできる公開valが4つあり、それぞれこのガイドの一部をカバーしています。

コメント付きブログ (ライブ) は、各投稿の下にコメントスレッドがあり、インデックスページにはコメント数が一括表示されるMarkdownブログです。リミックスした瞬間に動作し、環境変数を1つ設定するだけで自分のアカウントに向けられます。

SSOデモ (ライブ) は、訪問者をVal Townアカウントでサインインさせ、そのIDをウィジェットに渡すため、2度目のログインは不要です。

Webhookレシーバー (ライブ) は、すべての配信でHMAC署名を検証し、イベントをSQLiteに保存します。テストペイロードに署名して自分自身に配信するボタンが付いているため、実際のwebhookを設定する前に検証が成功する様子を確認できます。

エージェントスキル (ライブ) は、ウィジェット、SSO、REST API、モデレーション、Disqusからの移行をカバーするFastCommentsエージェントスキルのライブラリです。リミックスすると、Val TownのエージェントであるTownieがskills/から自動的にスキルを取り込むため、ドキュメントをチャットに貼り付けることなく、エージェントがコメントの設定方法を把握できます。

同じスキルはnpx skills add fastcomments/skillsを使って他の環境にもインストールできます。