ガイド 第10

同期モード

write-back vs read-only overlay・webhook HMAC・cron

@event/fours-sdk

イベントサイトと 4S の関係は 「4S が正、ローカルは overlay」 の一方向である。SDK は 4S へデータを書き戻さない。この章では接続モードの決まり方、3 層 overlay の合成、同期時のデータ損失防止、そして webhook / cron の実装状況を実 export で確認する。

接続モード matrix — createFoursConfig が env から一意に決める

FoursMode は 4 値。createFoursConfig() が env の組み合わせから優先順で決める(config.ts)。

mode条件(env)読み書き用途
mock-lockedUSE_MOCK_DATA✗(foursFetchPublic は null)UI 開発・4S 完全遮断
authenticatedFOURS_API_KEY ありwrite-capable transportservice / admin API
public-readFOURS_PUBLIC_READ✓(認証なし読み)公開読み取りのみ
stubそれ以外no-opno-op初期・未接続(render は落とさない)

判定は上から: USE_MOCK_DATAFOURS_API_KEYFOURS_PUBLIC_READstubsummarizeFoursConfig(config) が秘匿値を含まない診断(mode / apiBase / hasApiKey / hasWebhookSecret / hasServiceAuth)を返し、/api/4s/health 系のデバッグ UI に使える。

foursFetchPublicmock-locked / stub4S に一切出ず null を返す(fetchEvent も同様)。ただし profile.ts の一部関数は transport を経由せず生 fetch する点は第7章の注意を参照。

read-only overlay — なぜ書き戻さないか

4S 所有のフィールド(LOCKED)は 4S が唯一の真実源であり、CMS がそこへ書くと two-writer 衝突になる。だから CMS の編集はローカルの meta / content JSON にだけ書き、表示時に 4S へ重ねる。3 層の勝敗は固定:

Layer役割勝敗
14S APIcanonical / locked fieldslocked では 1 が勝つ
2meta overlayCMS 上書き常に最後に勝つ
3content/*.jsonfallback + CMS-only field 温存base

3 層 merge — merge.ts の pure 関数群

import {
  mergeCollectionCanonical, overlayLocked, safetyGuard,
} from "@event/fours-sdk";

const rows = mergeCollectionCanonical(
  layer1,                          // 4S(canonical。一覧の正)
  layer3,                          // content/*.json(CMS-only 温存)
  ["title", "startTime"],          // lockedKeys — 4S が非空なら勝つ
  metaById,                        // meta overlay(最後に勝つ)
);
関数動作
mergeById(base, overlay)id 一致で shallow merge、union
overlayLocked(base, locked, keys)lockednonEmpty な key のみ base に上書き(CMS-only は温存)
mergeCollection(l1, l3, keys, meta)union。4S に無いローカルレコードも残す(seed フェーズ用)
mergeCollectionCanonical(l1, l3, keys, meta)4S を一覧の正に。stale なローカル混入を防ぐ(旧実装の CMS 全損バグ対策)

seam は generic。SDK 自身はイベント固有の overlay 源(KV / content JSON)を知らず、@event/cms 側がこれらの関数へ layer3 / meta を注入する。

safetyGuard — 同期時のデータ損失防止

4S が一時的に空を返したときにローカルを全消去しないためのガード。sync ジョブは書き込み前に必ず通す。

existingfresh判定
< 5何でもok(seeding)
≥ 50skip(4S 障害の全消去防止)
≥ 5< existing × 0.30skip(大幅減を警戒)
force: trueok(明示上書き)

safetyGuard({ existing, fresh, force }) は理由付きの GuardDecisionok / reason / threshold)を返す。bool だけ欲しければ shouldSkip(existingArr, freshArr, { force })

write-back の現状 — 実装は無い(設計上そうしている)

SDK に 4S へデータを書き戻す関数は存在しないauthenticated mode(FOURS_API_KEY)は foursFetch の write-capable transport(method: "POST" | "PATCH" | "PUT" | "DELETE")を有効化するが、現状それを使って 4S のセッション / 登壇者を書く SDK 関数は無い。service.tsPOST も Cognito 認証と /auth/login のみで、データ書き込みではない。つまり「write-back」は将来の拡張余地であって、現行の同期は read-only overlay 一方向である。

webhook HMAC — 検証プリミティブはある / route は app 側

4S からの inbound webhook(例 POST /api/4s/webhook)の署名検証は verifyWebhookSignature が担う。raw body に対する HMAC-SHA256(hex、sha256= プレフィックス任意)。WebCrypto 実装なので edge / node 両対応、crypto.subtle.verify は constant-time。

// app/api/4s/webhook/route.ts
import { createFoursConfig, verifyWebhookSignature } from "@event/fours-sdk";

export async function POST(req: Request) {
  const raw = await req.text();                       // 生の body で検証する
  const sig = req.headers.get("x-4s-signature");      // "sha256=<hex>"
  const { webhookSecret } = createFoursConfig();
  if (!(await verifyWebhookSignature(raw, sig, webhookSecret))) {
    return new Response("invalid signature", { status: 401 });
  }
  // 検証後: safetyGuard 越しに revalidateTag("4s:event") など
  return Response.json({ ok: true });
}

route handler factory は SDK に無い — 検証関数だけを提供し、mount と後処理は app 側の責務。secret は config.webhookSecretFOURS_WEBHOOK_SECRET)。未設定 or 署名欠落なら verifyWebhookSignaturefalse を返す(fail-closed)。

cron — CRON_SECRET を渡すだけ / handler は無い

定期再同期を cron で回す場合も、SDK は config.cronSecretCRON_SECRET)を config に載せるだけで、cron route factory は提供しない。app 側が Bearer を照合し、safetyGuard 越しに 4S → ローカル overlay を再構築する。

// app/api/cron/sync-4s/route.ts
import { createFoursConfig, fetchEvent, safetyGuard } from "@event/fours-sdk";

export async function GET(req: Request) {
  const config = createFoursConfig();
  const auth = req.headers.get("authorization");
  if (auth !== `Bearer ${config.cronSecret}`) {
    return new Response("unauthorized", { status: 401 });
  }
  const event = await fetchEvent(config);             // mock/stub では null
  // safetyGuard({ existing, fresh }) を通してから overlay を書き換える
  return Response.json({ ok: true });
}

まとめると、webhook / cron は 「4S の変化を検知してローカルの cache / overlay を作り直す」入口であり、いずれも 4S を書き換えない。SDK が渡すのは検証材料(webhookSecret / cronSecret)と読み取り関数だけである。