defineRealms — N-named-realm 共有パスワード認証
xdeal は admin / roster の2 realm を別々に手書きしていた。defineRealms は realm を宣言的に定義し、cookie 名・パスワード env・ガードを一括生成する:
import { defineRealms } from "@event/shell";
export const realms = defineRealms({
admin: { cookieName: "myevent_admin_session" }, // ADMIN_PASSWORD
roster: { cookieName: "myevent_roster_session", passwordEnv: "ROSTER_PASSWORD" },
});
// server action 冒頭で必ず:
realms.admin.requireFromValue((await cookies()).get(realms.admin.cookieName)?.value);
照合は constantTimeEqual(文字単位 XOR・Edge 互換)。spikes の平文 === をここで是正した。cookie の読み書きはアプリ側が next/headers で行い、package は next 非依存を保つ。統治原則は第9章と同じ — middleware は「境界」であって「認可」ではない。route / action は必ず requireFromValue() で再検査する。
createNavRegistry — self-register 方式
3アプリの NAV_GROUPS はいずれも中央のハードコード配列で、ページ追加のたびに編集され、既にドリフトしていた。registry 方式では各機能が NavItem を宣言し、shell が収集・整列・権限フィルタする。ページ追加 = nav 定義1つ追加であり、中央配列の編集は発生しない。
export interface NavItem {
id: string; // 重複 register は無害化(HMR の二重登録対策)
group: string; // グループ見出し
label: string;
href: string;
order?: number; // グループ内の並び。既定 100
code?: string; // 短い略号バッジ(例 "GEN")
external?: boolean; // 新規タブ + noopener
requires?: string; // この realm を持つユーザーにだけ表示
}
nav.register({ id: "surveys", group: "運用", label: "サーベイ", href: "/admin/surveys", requires: "admin" });
const groups = nav.groups((realm) => hasRealm(realm)); // NavGroup[]
AdminNav
@event/shell/ui の <AdminNav groups={...}> が registry の出力を描画する。折りたたみグループ + query-aware active 判定 — href の ?tab=x まで一致して初めて active になるため、同一パスにタブ違いの項目が並ぶ nav でも正しくハイライトされる。useSearchParams を使うため、mount 側で <Suspense> boundary が必要である:
<Suspense>
<AdminNav groups={nav.groups(() => true)} />
</Suspense>
ChromeGate — ISR-safe な chrome 分岐
公開 chrome(Header / Footer)を /embed や /admin のような bare ページで外すゲート。実装上の要点は判定手段にある: root layout で headers() を読むと全ページが force-dynamic 化してエッジキャッシュが死ぬ(前日トラフィックで 15 秒スパイクの実績がある)。ChromeGate は静的レンダリングと両立する usePathname() で判定する。
<ChromeGate barePatterns={[/^\/embed/, /^\/admin/]} header={<Header />} footer={<Footer />}>
{children}
</ChromeGate>
header / footer は server component をスロット(props)として受け取り、配置だけを行う。
devLogin — 三重ガード付きワンクリックログイン
devhub の「開く」ボタンが GET /login/dev?from=/admin を叩いて自動ログインする導線。createDevLoginHandler({ cookieName }) を mount する。安全不変条件は3つ:
- 本番(NODE_ENV=production)は通常
/loginへ倒す — 本番に dev バックドアは存在しない - localhost 直アクセスのみ — host が
localhost/127.0.0.1に一致し、かつcf-connecting-ipヘッダが無いこと(Cloudflare トンネル経由 = 外部到達は拒否) - open-redirect 防止 —
fromは自サイト内絶対パス(/始まり・//拒否)のみ許可
パスワードは URL を通らない。サーバが env の ADMIN_PASSWORD を読んで cookie を張るだけである。
createAuthGateMiddleware
/admin /api/admin の境界ゲート。dev は素通し(auth 側の dev bypass と二重で安全)、bypassPrefixes(例 /api/auth/)は素通し、API パスには 401 JSON、ページには /login?from=<path> redirect を返す。edge runtime で動くため、@event/core barrel ではなく @event/core/auth subpath から constantTimeEqual を import している — barrel 経由だと contentStore の node:fs / node:crypto が edge bundle に混入する(第46章)。