イベントサイトと 4S の関係は 「4S が正、ローカルは overlay」 の一方向である。SDK は 4S へデータを書き戻さない。この章では接続モードの決まり方、3 層 overlay の合成、同期時のデータ損失防止、そして webhook / cron の実装状況を実 export で確認する。
接続モード matrix — createFoursConfig が env から一意に決める
FoursMode は 4 値。createFoursConfig() が env の組み合わせから優先順で決める(config.ts)。
| mode | 条件(env) | 読み | 書き | 用途 |
|---|---|---|---|---|
mock-locked | USE_MOCK_DATA | ✗(foursFetchPublic は null) | ✗ | UI 開発・4S 完全遮断 |
authenticated | FOURS_API_KEY あり | ✓ | write-capable transport | service / admin API |
public-read | FOURS_PUBLIC_READ | ✓(認証なし読み) | ✗ | 公開読み取りのみ |
stub | それ以外 | no-op | no-op | 初期・未接続(render は落とさない) |
判定は上から: USE_MOCK_DATA → FOURS_API_KEY → FOURS_PUBLIC_READ → stub。summarizeFoursConfig(config) が秘匿値を含まない診断(mode / apiBase / hasApiKey / hasWebhookSecret / hasServiceAuth)を返し、/api/4s/health 系のデバッグ UI に使える。
foursFetchPublic は mock-locked / stub で 4S に一切出ず null を返す(fetchEvent も同様)。ただし profile.ts の一部関数は transport を経由せず生 fetch する点は第7章の注意を参照。
read-only overlay — なぜ書き戻さないか
4S 所有のフィールド(LOCKED)は 4S が唯一の真実源であり、CMS がそこへ書くと two-writer 衝突になる。だから CMS の編集はローカルの meta / content JSON にだけ書き、表示時に 4S へ重ねる。3 層の勝敗は固定:
| Layer | 源 | 役割 | 勝敗 |
|---|---|---|---|
| 1 | 4S API | canonical / locked fields | locked では 1 が勝つ |
| 2 | meta overlay | CMS 上書き | 常に最後に勝つ |
| 3 | content/*.json | fallback + 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) | locked の nonEmpty な 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 ジョブは書き込み前に必ず通す。
| existing | fresh | 判定 |
|---|---|---|
| < 5 | 何でも | ok(seeding) |
| ≥ 5 | 0 | skip(4S 障害の全消去防止) |
| ≥ 5 | < existing × 0.30 | skip(大幅減を警戒) |
| — | force: true | ok(明示上書き) |
safetyGuard({ existing, fresh, force }) は理由付きの GuardDecision(ok / 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.ts の POST も 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.webhookSecret(FOURS_WEBHOOK_SECRET)。未設定 or 署名欠落なら verifyWebhookSignature は false を返す(fail-closed)。
cron — CRON_SECRET を渡すだけ / handler は無い
定期再同期を cron で回す場合も、SDK は config.cronSecret(CRON_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)と読み取り関数だけである。