From 8d5787e56e8458539c13d310970602b1d19df604 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 23 Jul 2026 09:31:04 +0200 Subject: [PATCH] feat: self-serve developer access for paid tiers, split BYOK into its own permission (v0.72.0) Splits what was one isDeveloper flag (gating webhooks, API keys, AND BYOK together) into two independent permissions: - isDeveloper (webhooks + self-serve API keys): admin-toggled as before, but now ALSO self-serve -- PATCH /api/v1/users/me/developer-access lets any paid-tier (tier !== "free") user turn it on themselves, no added fee. Free tier still needs an admin grant. Turning it off is always self-serve regardless of tier, since revoking your own access needs no gatekeeping. canSelfServeDeveloperAccess() in lib/permissions.ts is the single check for "is this tier eligible." - isByokEnabled (BYOK AI provider keys): new column, admin-only, no self-serve path at all -- routing real AI provider spend through Epicure on the user's own key warrants a manual admin check-in that webhooks/API access don't need. requireByok() replaces requireDeveloper() on the three ai-keys routes. Migration adds is_byok_enabled and grandfathers in anyone who already has a BYOK key configured (the earlier grandfather migration only covered the combined isDeveloper flag, which BYOK no longer reads). Settings UI: webhooks/API-keys pages show a self-serve "Enable" toggle for paid-tier non-developers instead of the locked notice (still shown to free-tier users), plus a "Disable developer access" link once enabled. Settings -> AI's BYOK section now checks isByokEnabled instead of isDeveloper -- unaffected by the self-serve change, still fully admin-gated. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 5 + FEATURE_AUDIT.md | 2 +- apps/web/app/(app)/settings/ai/page.tsx | 6 +- apps/web/app/(app)/settings/api-keys/page.tsx | 33 +- apps/web/app/(app)/settings/layout.tsx | 6 +- apps/web/app/(app)/settings/webhooks/page.tsx | 33 +- apps/web/app/admin/users/[id]/page.tsx | 2 +- apps/web/app/api/v1/admin/users/[id]/route.ts | 14 +- .../app/api/v1/ai-keys/[provider]/route.ts | 4 +- apps/web/app/api/v1/ai-keys/route.ts | 6 +- .../api/v1/users/me/developer-access/route.ts | 35 + apps/web/components/admin/user-editor.tsx | 15 +- .../settings/developer-access-toggle.tsx | 44 + .../disable-developer-access-button.tsx | 40 + .../components/settings/settings-sidebar.tsx | 4 +- apps/web/lib/api-auth.ts | 33 +- apps/web/lib/changelog.ts | 9 +- apps/web/lib/openapi.ts | 12 +- apps/web/lib/permissions.ts | 24 +- apps/web/messages/en.json | 4 +- apps/web/messages/fr.json | 4 +- apps/web/package.json | 2 +- package.json | 2 +- .../src/migrations/0061_stormy_the_fallen.sql | 9 + .../db/src/migrations/meta/0061_snapshot.json | 6039 +++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/users.ts | 12 +- 27 files changed, 6338 insertions(+), 68 deletions(-) create mode 100644 apps/web/app/api/v1/users/me/developer-access/route.ts create mode 100644 apps/web/components/settings/developer-access-toggle.tsx create mode 100644 apps/web/components/settings/disable-developer-access-button.tsx create mode 100644 packages/db/src/migrations/0061_stormy_the_fallen.sql create mode 100644 packages/db/src/migrations/meta/0061_snapshot.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a060c..80d63ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.72.0 — 2026-07-23 09:30 + +### Added +- Developer access (webhooks + API keys) is now self-serve for any paid-tier user, free of charge — enable it yourself in Settings instead of waiting on an admin. Free tier still needs an admin grant. BYOK is now a fully separate permission, admin-only, unaffected by the self-serve change. + ## 0.71.0 — 2026-07-22 10:00 ### Added diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md index bb54cd4..9aa7ff4 100644 --- a/FEATURE_AUDIT.md +++ b/FEATURE_AUDIT.md @@ -105,7 +105,7 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real | **Self-serve billing portal** | **Missing** | No Stripe customer portal; tier changes today only happen via direct admin edit at `/admin/users/[id]` | — | | Admin dashboard | Exists | 13 sections: overview, insights/analytics, users, invites, recipe moderation, reports, support, tiers, webhooks, audit logs, storage, AI config, site settings, changelog | `apps/web/app/admin/**` | | Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags gate 3 AI capabilities by tier; prefs let users hide 7 nav sections, no billing implication | `apps/web/lib/{feature-flags,feature-prefs}.ts` | -| **Developer permission** (new, 2026-07-22) | Exists | A fourth, orthogonal access dimension — `users.isDeveloper`, admin-toggled in `admin/users/[id]`, gates user webhooks + self-serve API keys + BYOK. Previously all three had zero gating (any logged-in user, any tier). Existing users with a webhook/API key/BYOK key were grandfathered in by the migration. Deliberately a single `hasDeveloperAccess()` function so a future subscription-tier auto-grant is a one-line change, not a redesign. | `apps/web/lib/permissions.ts`, `apps/web/lib/api-auth.ts` (`requireDeveloper`) | +| **Developer permission** (2026-07-22, split 2026-07-23) | Exists | Two separate, orthogonal permissions — not one. `users.isDeveloper` gates webhooks + self-serve API keys: admin-toggled always, *and* self-serve toggleable by the user themselves once on a paid tier (no added fee) via `PATCH /api/v1/users/me/developer-access`; free-tier users still need an admin grant. `users.isByokEnabled` gates BYOK separately, admin-only, no self-serve — routing real AI spend through Epicure on the user's own key warrants a manual check-in. Previously all three (webhooks/API keys/BYOK) shared one flag with zero self-serve path. Existing users with a webhook/API key were already grandfathered for `isDeveloper`; a second migration grandfathered existing BYOK users into `isByokEnabled` specifically. | `apps/web/lib/permissions.ts` (`hasDeveloperAccess`, `hasByokAccess`, `canSelfServeDeveloperAccess`), `apps/web/lib/api-auth.ts` (`requireDeveloper`, `requireByok`) | | User webhooks (personal automation) | Exists | 7 events, Zapier-style, requires developer access | `apps/web/lib/webhooks.ts` | | Admin ops webhooks (site-wide) | Exists | 3 events (signup, ticket, report) — admin-only, unrelated to developer access | `apps/web/lib/admin-webhooks.ts` | | Support tickets ↔ Gitea two-way sync | Exists, verified real | Outbound (create/comment/close mirror to Gitea issue) + inbound (signed webhook receiver, dedup, loop-safe) | `apps/web/lib/gitea.ts`, `apps/web/app/api/webhooks/gitea/route.ts` | diff --git a/apps/web/app/(app)/settings/ai/page.tsx b/apps/web/app/(app)/settings/ai/page.tsx index 440fe08..e69e4d1 100644 --- a/apps/web/app/(app)/settings/ai/page.tsx +++ b/apps/web/app/(app)/settings/ai/page.tsx @@ -28,7 +28,7 @@ export default async function AiSettingsPage() { }), // Tier comes from the DB, not the (up to 5-minute-stale) session cookie // cache, so a just-changed tier's limits show up immediately here. - db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { tier: true, isDeveloper: true } }), + db.query.users.findFirst({ where: eq(users.id, session.user.id), columns: { tier: true, isByokEnabled: true } }), ]); const [tierDef, usage, recipeCount, storageUsedMb] = await Promise.all([ @@ -81,10 +81,10 @@ export default async function AiSettingsPage() { {m.settings.byok.description}

- {dbUser?.isDeveloper ? ( + {dbUser?.isByokEnabled ? ( k.provider)} /> ) : ( - + )} diff --git a/apps/web/app/(app)/settings/api-keys/page.tsx b/apps/web/app/(app)/settings/api-keys/page.tsx index 9028c26..f8ddd02 100644 --- a/apps/web/app/(app)/settings/api-keys/page.tsx +++ b/apps/web/app/(app)/settings/api-keys/page.tsx @@ -6,6 +6,9 @@ import { auth } from "@/lib/auth/server"; import { db, apiKeys, users, eq } from "@epicure/db"; import { ApiKeysManager } from "@/components/settings/api-keys-manager"; import { DeveloperLockedNotice } from "@/components/settings/developer-locked-notice"; +import { DeveloperAccessToggle } from "@/components/settings/developer-access-toggle"; +import { DisableDeveloperAccessButton } from "@/components/settings/disable-developer-access-button"; +import { canSelfServeDeveloperAccess } from "@/lib/permissions"; import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = {}; @@ -15,7 +18,7 @@ export default async function ApiKeysPage() { if (!session) return null; const m = getMessages((session.user as { locale?: string }).locale); - const dbUser = (await db.select({ isDeveloper: users.isDeveloper }).from(users).where(eq(users.id, session.user.id)).limit(1))[0]; + const dbUser = (await db.select({ isDeveloper: users.isDeveloper, tier: users.tier }).from(users).where(eq(users.id, session.user.id)).limit(1))[0]; const keys = dbUser?.isDeveloper ? await db @@ -47,17 +50,25 @@ export default async function ApiKeysPage() { {m.settings.apiKeysPage.docsLink} - {!dbUser?.isDeveloper && } + {!dbUser?.isDeveloper && dbUser && canSelfServeDeveloperAccess(dbUser) && ( + + )} + {!dbUser?.isDeveloper && (!dbUser || !canSelfServeDeveloperAccess(dbUser)) && ( + + )} {dbUser?.isDeveloper && ( - ({ - id: k.id, - name: k.name, - scope: k.scope, - lastUsedAt: k.lastUsedAt ? k.lastUsedAt.toISOString() : null, - createdAt: k.createdAt.toISOString(), - }))} - /> + <> + ({ + id: k.id, + name: k.name, + scope: k.scope, + lastUsedAt: k.lastUsedAt ? k.lastUsedAt.toISOString() : null, + createdAt: k.createdAt.toISOString(), + }))} + /> + + )} diff --git a/apps/web/app/(app)/settings/layout.tsx b/apps/web/app/(app)/settings/layout.tsx index 9908829..c3db5dc 100644 --- a/apps/web/app/(app)/settings/layout.tsx +++ b/apps/web/app/(app)/settings/layout.tsx @@ -3,14 +3,16 @@ import { auth } from "@/lib/auth/server"; import { db, users, eq } from "@epicure/db"; import { SettingsSidebar } from "@/components/settings/settings-sidebar"; import { getMessages } from "@/lib/i18n/server"; +import { canSelfServeDeveloperAccess } from "@/lib/permissions"; export default async function SettingsLayout({ children }: { children: React.ReactNode }) { const session = await auth.api.getSession({ headers: await headers() }); const m = getMessages((session?.user as { locale?: string })?.locale); const dbUser = session - ? (await db.select({ isDeveloper: users.isDeveloper }).from(users).where(eq(users.id, session.user.id)).limit(1))[0] + ? (await db.select({ isDeveloper: users.isDeveloper, tier: users.tier }).from(users).where(eq(users.id, session.user.id)).limit(1))[0] : undefined; + const showDeveloperNav = !!dbUser && (dbUser.isDeveloper || canSelfServeDeveloperAccess(dbUser)); return (
@@ -19,7 +21,7 @@ export default async function SettingsLayout({ children }: { children: React.Rea

{m.settings.subtitle}

- +
{children}
diff --git a/apps/web/app/(app)/settings/webhooks/page.tsx b/apps/web/app/(app)/settings/webhooks/page.tsx index f43f57a..e91e39d 100644 --- a/apps/web/app/(app)/settings/webhooks/page.tsx +++ b/apps/web/app/(app)/settings/webhooks/page.tsx @@ -4,6 +4,9 @@ import { auth } from "@/lib/auth/server"; import { db, webhooks, users, eq } from "@epicure/db"; import { WebhooksManager } from "@/components/settings/webhooks-manager"; import { DeveloperLockedNotice } from "@/components/settings/developer-locked-notice"; +import { DeveloperAccessToggle } from "@/components/settings/developer-access-toggle"; +import { DisableDeveloperAccessButton } from "@/components/settings/disable-developer-access-button"; +import { canSelfServeDeveloperAccess } from "@/lib/permissions"; import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = {}; @@ -13,7 +16,7 @@ export default async function WebhooksPage() { if (!session) return null; const m = getMessages((session.user as { locale?: string }).locale); - const dbUser = (await db.select({ isDeveloper: users.isDeveloper }).from(users).where(eq(users.id, session.user.id)).limit(1))[0]; + const dbUser = (await db.select({ isDeveloper: users.isDeveloper, tier: users.tier }).from(users).where(eq(users.id, session.user.id)).limit(1))[0]; const rows = dbUser?.isDeveloper ? await db @@ -37,17 +40,25 @@ export default async function WebhooksPage() { {m.settings.webhooksPage.description}

- {!dbUser?.isDeveloper && } + {!dbUser?.isDeveloper && dbUser && canSelfServeDeveloperAccess(dbUser) && ( + + )} + {!dbUser?.isDeveloper && (!dbUser || !canSelfServeDeveloperAccess(dbUser)) && ( + + )} {dbUser?.isDeveloper && ( - ({ - id: w.id, - url: w.url, - events: w.events, - active: w.active, - createdAt: w.createdAt.toISOString(), - }))} - /> + <> + ({ + id: w.id, + url: w.url, + events: w.events, + active: w.active, + createdAt: w.createdAt.toISOString(), + }))} + /> + + )} diff --git a/apps/web/app/admin/users/[id]/page.tsx b/apps/web/app/admin/users/[id]/page.tsx index 8818e08..18e7a26 100644 --- a/apps/web/app/admin/users/[id]/page.tsx +++ b/apps/web/app/admin/users/[id]/page.tsx @@ -102,7 +102,7 @@ export default async function AdminUserDetailPage({ params }: PageProps) { Edit Role & Tier - + diff --git a/apps/web/app/api/v1/admin/users/[id]/route.ts b/apps/web/app/api/v1/admin/users/[id]/route.ts index 6f6176b..fb978a6 100644 --- a/apps/web/app/api/v1/admin/users/[id]/route.ts +++ b/apps/web/app/api/v1/admin/users/[id]/route.ts @@ -12,8 +12,8 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) { if (response) return response; const { id } = await params; - const body = await req.json() as { role?: string; tier?: string; isDeveloper?: boolean }; - const { role, tier, isDeveloper } = body; + const body = await req.json() as { role?: string; tier?: string; isDeveloper?: boolean; isByokEnabled?: boolean }; + const { role, tier, isDeveloper, isByokEnabled } = body; const validRoles = ["user", "moderator", "admin"] as const; const validTiers = ["free", "pro", "family"] as const; @@ -27,19 +27,23 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) { if (isDeveloper !== undefined && typeof isDeveloper !== "boolean") { return NextResponse.json({ error: "Invalid isDeveloper" }, { status: 400 }); } + if (isByokEnabled !== undefined && typeof isByokEnabled !== "boolean") { + return NextResponse.json({ error: "Invalid isByokEnabled" }, { status: 400 }); + } - const updateData: Partial<{ role: "user" | "moderator" | "admin"; tier: "free" | "pro" | "family"; isDeveloper: boolean; updatedAt: Date }> = { + const updateData: Partial<{ role: "user" | "moderator" | "admin"; tier: "free" | "pro" | "family"; isDeveloper: boolean; isByokEnabled: boolean; updatedAt: Date }> = { updatedAt: new Date(), }; if (role) updateData.role = role as "user" | "moderator" | "admin"; if (tier) updateData.tier = tier as "free" | "pro" | "family"; if (isDeveloper !== undefined) updateData.isDeveloper = isDeveloper; + if (isByokEnabled !== undefined) updateData.isByokEnabled = isByokEnabled; const [updated] = await db .update(users) .set(updateData) .where(eq(users.id, id)) - .returning({ id: users.id, role: users.role, tier: users.tier, isDeveloper: users.isDeveloper }); + .returning({ id: users.id, role: users.role, tier: users.tier, isDeveloper: users.isDeveloper, isByokEnabled: users.isByokEnabled }); if (!updated) { return NextResponse.json({ error: "User not found" }, { status: 404 }); @@ -52,7 +56,7 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) { action: "admin.user.update", targetType: "user", targetId: id, - metadata: JSON.stringify({ role, tier, isDeveloper }), + metadata: JSON.stringify({ role, tier, isDeveloper, isByokEnabled }), createdAt: new Date(), }); diff --git a/apps/web/app/api/v1/ai-keys/[provider]/route.ts b/apps/web/app/api/v1/ai-keys/[provider]/route.ts index 30103ca..51c222e 100644 --- a/apps/web/app/api/v1/ai-keys/[provider]/route.ts +++ b/apps/web/app/api/v1/ai-keys/[provider]/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from "next/server"; import { db, userAiKeys, eq, and } from "@epicure/db"; -import { requireDeveloper } from "@/lib/api-auth"; +import { requireByok } from "@/lib/api-auth"; type Params = { params: Promise<{ provider: string }> }; export async function DELETE(_req: Request, { params }: Params) { - const { session, response } = await requireDeveloper(); + const { session, response } = await requireByok(); if (response) return response; const { provider } = await params; diff --git a/apps/web/app/api/v1/ai-keys/route.ts b/apps/web/app/api/v1/ai-keys/route.ts index 85f2a01..198dfa8 100644 --- a/apps/web/app/api/v1/ai-keys/route.ts +++ b/apps/web/app/api/v1/ai-keys/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { db, userAiKeys, eq, and } from "@epicure/db"; -import { requireDeveloper } from "@/lib/api-auth"; +import { requireByok } from "@/lib/api-auth"; import { encrypt } from "@/lib/encrypt"; import { applyRateLimit } from "@/lib/rate-limit"; @@ -13,7 +13,7 @@ const PostSchema = z.object({ }); export async function GET() { - const { session, response } = await requireDeveloper(); + const { session, response } = await requireByok(); if (response) return response; const keys = await db.query.userAiKeys.findMany({ @@ -25,7 +25,7 @@ export async function GET() { } export async function POST(req: Request) { - const { session, response } = await requireDeveloper(); + const { session, response } = await requireByok(); if (response) return response; const limited = await applyRateLimit(`rl:ai-keys:${session!.user.id}`, 5, 3600); diff --git a/apps/web/app/api/v1/users/me/developer-access/route.ts b/apps/web/app/api/v1/users/me/developer-access/route.ts new file mode 100644 index 0000000..367b080 --- /dev/null +++ b/apps/web/app/api/v1/users/me/developer-access/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { db, users, eq } from "@epicure/db"; +import { requireSession } from "@/lib/api-auth"; +import { canSelfServeDeveloperAccess } from "@/lib/permissions"; + +const PatchSchema = z.object({ enabled: z.boolean() }); + +/** Self-serve on/off for the same isDeveloper flag admin/users/[id] can + * also set — open to any paid tier, no added fee. Free tier still needs + * an admin grant (requireAdmin's path, unchanged). */ +export async function PATCH(req: NextRequest) { + const { session, response } = await requireSession(); + if (response) return response; + + const parsed = PatchSchema.safeParse(await req.json()); + if (!parsed.success) { + return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 }); + } + + // Turning it off is always allowed, regardless of tier or who granted + // it — only turning it ON self-serve requires a paid tier. + if (parsed.data.enabled) { + // Don't trust session.user.tier — up to 5-minute-stale cookieCache, + // same reasoning as every other fresh-role/tier re-query in this codebase. + const [dbUser] = await db.select({ tier: users.tier }).from(users).where(eq(users.id, session!.user.id)).limit(1); + if (!dbUser || !canSelfServeDeveloperAccess(dbUser)) { + return NextResponse.json({ error: "Upgrade to a paid plan to enable developer access yourself, or ask an admin" }, { status: 403 }); + } + } + + await db.update(users).set({ isDeveloper: parsed.data.enabled, updatedAt: new Date() }).where(eq(users.id, session!.user.id)); + + return NextResponse.json({ ok: true, isDeveloper: parsed.data.enabled }); +} diff --git a/apps/web/components/admin/user-editor.tsx b/apps/web/components/admin/user-editor.tsx index bb19ae9..7a995f4 100644 --- a/apps/web/components/admin/user-editor.tsx +++ b/apps/web/components/admin/user-editor.tsx @@ -18,12 +18,14 @@ interface UserEditorProps { currentRole: "user" | "moderator" | "admin"; currentTier: "free" | "pro" | "family"; currentIsDeveloper: boolean; + currentIsByokEnabled: boolean; } -export function UserEditor({ userId, currentRole, currentTier, currentIsDeveloper }: UserEditorProps) { +export function UserEditor({ userId, currentRole, currentTier, currentIsDeveloper, currentIsByokEnabled }: UserEditorProps) { const [role, setRole] = useState<"user" | "moderator" | "admin">(currentRole); const [tier, setTier] = useState<"free" | "pro" | "family">(currentTier); const [isDeveloper, setIsDeveloper] = useState(currentIsDeveloper); + const [isByokEnabled, setIsByokEnabled] = useState(currentIsByokEnabled); const [saving, setSaving] = useState(false); async function handleSave() { @@ -32,7 +34,7 @@ export function UserEditor({ userId, currentRole, currentTier, currentIsDevelope const res = await fetch(`/api/v1/admin/users/${userId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ role, tier, isDeveloper }), + body: JSON.stringify({ role, tier, isDeveloper, isByokEnabled }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); @@ -79,10 +81,17 @@ export function UserEditor({ userId, currentRole, currentTier, currentIsDevelope
-

Enables webhooks, self-serve API keys, and BYOK AI provider keys.

+

Enables webhooks and self-serve API keys. Users on a paid tier can also self-enable this.

+
+
+ +

Enables bring-your-own AI provider keys. Admin-only, no self-serve.

+
+ +
+
+ ); +} diff --git a/apps/web/components/settings/disable-developer-access-button.tsx b/apps/web/components/settings/disable-developer-access-button.tsx new file mode 100644 index 0000000..2e1707b --- /dev/null +++ b/apps/web/components/settings/disable-developer-access-button.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; + +/** Turning developer access off is always self-serve regardless of tier + * or who granted it — only turning it back on (for a free-tier user) + * requires an admin. */ +export function DisableDeveloperAccessButton() { + const router = useRouter(); + const [disabling, setDisabling] = useState(false); + + async function disable() { + setDisabling(true); + try { + const res = await fetch("/api/v1/users/me/developer-access", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: false }), + }); + if (!res.ok) throw new Error(); + router.refresh(); + } catch { + toast.error("Couldn't disable developer access"); + setDisabling(false); + } + } + + return ( + + ); +} diff --git a/apps/web/components/settings/settings-sidebar.tsx b/apps/web/components/settings/settings-sidebar.tsx index 745094e..2900fd8 100644 --- a/apps/web/components/settings/settings-sidebar.tsx +++ b/apps/web/components/settings/settings-sidebar.tsx @@ -19,11 +19,11 @@ const NAV_ITEMS = [ const DEVELOPER_ONLY_KEYS = new Set(["apiKeys", "webhooks"]); -export function SettingsSidebar({ isDeveloper }: { isDeveloper: boolean }) { +export function SettingsSidebar({ showDeveloperNav }: { showDeveloperNav: boolean }) { const pathname = usePathname(); const t = useTranslations("settings"); - const visibleItems = NAV_ITEMS.filter((item) => isDeveloper || !DEVELOPER_ONLY_KEYS.has(item.key)); + const visibleItems = NAV_ITEMS.filter((item) => showDeveloperNav || !DEVELOPER_ONLY_KEYS.has(item.key)); return (