From 8d0a28c7ba6251d158adab13a8396d50b5b57c95 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Mon, 27 Apr 2026 09:02:12 +0100 Subject: [PATCH] feat(demo): lock down identity, egress, and credential surfaces in demo mode Adds denyInDemo() middleware to mutations that would otherwise let a public demo user mint credentials, escalate privileges, configure outbound HTTP destinations, exfiltrate data, or stand up real fleet infrastructure. Routers covered: - user: changePassword, updateProfile, setupTotp, verifyAndEnableTotp, disableTotp - team: create, delete, rename, addMember, removeMember, updateMemberRole, lockMember, unlockMember, resetMemberPassword, updateRequireTwoFactor, updateAvailableTags, updateDefaultEnvironment, linkMemberToOidc, updateAiConfig, testAiConnection - settings: updateOidc/RoleMapping/TeamMappings, updateFleet, updateAnomalyConfig, testOidc, all backup/restore/storage mutations, updateScim, generateScimToken - environment: create, update, delete, generateEnrollmentToken, revokeEnrollmentToken - git-sync: retryAllFailed, retryJob - alert-channels: createChannel, updateChannel, deleteChannel, testChannel - webhook-endpoint: create, update, delete, toggleEnabled, testDelivery - secret: create, update, delete - certificate: upload, delete REST surface: - POST /api/agent/enroll: short-circuits with 403 in demo mode before any database lookup, so demo enrollment tokens are example-only. All existing router tests pass unchanged because vi.mock factories already expose denyInDemo as a passthrough. Adds a focused test confirming the enroll endpoint rejects in demo without touching the database. --- .../api/agent/enroll/__tests__/route.test.ts | 28 ++++++++++++++++++- src/app/api/agent/enroll/route.ts | 8 ++++++ src/server/routers/alert-channels.ts | 6 +++- src/server/routers/certificate.ts | 4 ++- src/server/routers/environment.ts | 5 ++++ src/server/routers/git-sync.ts | 4 ++- src/server/routers/secret.ts | 5 +++- src/server/routers/settings.ts | 17 ++++++++++- src/server/routers/team.ts | 17 ++++++++++- src/server/routers/user.ts | 7 ++++- src/server/routers/webhook-endpoint.ts | 7 ++++- 11 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/app/api/agent/enroll/__tests__/route.test.ts b/src/app/api/agent/enroll/__tests__/route.test.ts index a7f2561b..842e45f4 100644 --- a/src/app/api/agent/enroll/__tests__/route.test.ts +++ b/src/app/api/agent/enroll/__tests__/route.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect, beforeEach } from "vitest"; +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; import { mockDeep, mockReset, type DeepMockProxy } from "vitest-mock-extended"; import type { PrismaClient } from "@/generated/prisma"; @@ -164,3 +164,29 @@ describe("POST /api/agent/enroll -- NODE-03 label template auto-assignment", () expect(prismaMock.vectorNode.update).not.toHaveBeenCalled(); }); }); + +describe("POST /api/agent/enroll -- demo mode", () => { + const ORIGINAL_ENV = process.env.NEXT_PUBLIC_VF_DEMO_MODE; + + beforeEach(() => { + mockReset(prismaMock); + }); + + afterEach(() => { + if (ORIGINAL_ENV === undefined) delete process.env.NEXT_PUBLIC_VF_DEMO_MODE; + else process.env.NEXT_PUBLIC_VF_DEMO_MODE = ORIGINAL_ENV; + }); + + it("rejects with 403 when demo mode is active and never touches the database", async () => { + process.env.NEXT_PUBLIC_VF_DEMO_MODE = "true"; + + const req = makeRequest({ token: "vf_enroll_demo", hostname: "demo-host" }); + const res = await POST(req); + + expect(res.status).toBe(403); + const body = await res.json(); + expect(body.error).toMatch(/demo/i); + expect(prismaMock.environment.findMany).not.toHaveBeenCalled(); + expect(prismaMock.vectorNode.create).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index b6623695..6e072e43 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -6,6 +6,7 @@ import { fireEventAlert } from "@/server/services/event-alerts"; import { debugLog, errorLog } from "@/lib/logger"; import { nodeMatchesGroup } from "@/lib/node-group-utils"; import { checkIpRateLimit } from "@/app/api/_lib/ip-rate-limit"; +import { isDemoMode } from "@/lib/is-demo-mode"; const enrollSchema = z.object({ token: z.string().min(1), @@ -17,6 +18,13 @@ const enrollSchema = z.object({ }); export async function POST(request: Request) { + if (isDemoMode()) { + return NextResponse.json( + { error: "Agent enrollment is disabled in the public demo." }, + { status: 403 }, + ); + } + const rateLimited = checkIpRateLimit(request, "enroll", 10); if (rateLimited) return rateLimited; diff --git a/src/server/routers/alert-channels.ts b/src/server/routers/alert-channels.ts index 98b089d0..bb297e8a 100644 --- a/src/server/routers/alert-channels.ts +++ b/src/server/routers/alert-channels.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { Prisma } from "@/generated/prisma"; -import { router, protectedProcedure, withTeamAccess } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import { withAudit } from "@/server/middleware/audit"; import { validatePublicUrl, validateSmtpHost } from "@/server/services/url-validation"; @@ -52,6 +52,7 @@ export const alertChannelsRouter = router({ config: z.record(z.string(), z.unknown()), }), ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("notificationChannel.created", "NotificationChannel")) .mutation(async ({ input }) => { @@ -124,6 +125,7 @@ export const alertChannelsRouter = router({ enabled: z.boolean().optional(), }), ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("notificationChannel.updated", "NotificationChannel")) .mutation(async ({ input }) => { @@ -205,6 +207,7 @@ export const alertChannelsRouter = router({ deleteChannel: protectedProcedure .input(z.object({ id: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("notificationChannel.deleted", "NotificationChannel")) .mutation(async ({ input }) => { @@ -224,6 +227,7 @@ export const alertChannelsRouter = router({ testChannel: protectedProcedure .input(z.object({ id: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("notificationChannel.tested", "NotificationChannel")) .mutation(async ({ input }) => { diff --git a/src/server/routers/certificate.ts b/src/server/routers/certificate.ts index dad75713..b13f7ae5 100644 --- a/src/server/routers/certificate.ts +++ b/src/server/routers/certificate.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure, withTeamAccess } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import { encrypt, decrypt } from "@/server/services/crypto"; import { parseCertExpiry, daysUntilExpiry } from "@/server/services/cert-expiry-checker"; @@ -81,6 +81,7 @@ export const certificateRouter = router({ dataBase64: z.string().min(1), }), ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("certificate.uploaded", "Certificate")) .mutation(async ({ input }) => { @@ -115,6 +116,7 @@ export const certificateRouter = router({ delete: protectedProcedure .input(z.object({ id: z.string(), environmentId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("certificate.deleted", "Certificate")) .mutation(async ({ input }) => { diff --git a/src/server/routers/environment.ts b/src/server/routers/environment.ts index 62ed3975..fb7105e8 100644 --- a/src/server/routers/environment.ts +++ b/src/server/routers/environment.ts @@ -86,6 +86,7 @@ export const environmentRouter = router({ teamId: z.string(), }) ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("environment.created", "Environment")) .mutation(async ({ input }) => { @@ -125,6 +126,7 @@ export const environmentRouter = router({ costBudgetCents: z.number().int().min(0).max(1_000_000_00).nullable().optional(), // monthly budget in cents, null to disable }) ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("environment.updated", "Environment")) .mutation(async ({ input, ctx }) => { @@ -264,6 +266,7 @@ export const environmentRouter = router({ delete: protectedProcedure .input(z.object({ id: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("environment.deleted", "Environment")) .mutation(async ({ input }) => { @@ -295,6 +298,7 @@ export const environmentRouter = router({ generateEnrollmentToken: protectedProcedure .input(z.object({ environmentId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("environment.enrollmentToken.generated", "Environment")) .mutation(async ({ input }) => { @@ -318,6 +322,7 @@ export const environmentRouter = router({ revokeEnrollmentToken: protectedProcedure .input(z.object({ environmentId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("environment.enrollmentToken.revoked", "Environment")) .mutation(async ({ input }) => { diff --git a/src/server/routers/git-sync.ts b/src/server/routers/git-sync.ts index 68ce1f0b..6d6f9ef3 100644 --- a/src/server/routers/git-sync.ts +++ b/src/server/routers/git-sync.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure, withTeamAccess } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; export const gitSyncRouter = router({ @@ -84,6 +84,7 @@ export const gitSyncRouter = router({ /** Retry all failed jobs for an environment. */ retryAllFailed: protectedProcedure .input(z.object({ environmentId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .mutation(async ({ input }) => { const now = new Date(); @@ -105,6 +106,7 @@ export const gitSyncRouter = router({ /** Retry a single failed job. */ retryJob: protectedProcedure .input(z.object({ jobId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .mutation(async ({ input }) => { const job = await prisma.gitSyncJob.findUnique({ diff --git a/src/server/routers/secret.ts b/src/server/routers/secret.ts index 5a0eef88..a5664b13 100644 --- a/src/server/routers/secret.ts +++ b/src/server/routers/secret.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure, withTeamAccess } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import { encrypt, decrypt } from "@/server/services/crypto"; import { withAudit } from "@/server/middleware/audit"; @@ -28,6 +28,7 @@ export const secretRouter = router({ value: z.string().min(1), }), ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("secret.created", "Secret")) .mutation(async ({ input }) => { @@ -55,6 +56,7 @@ export const secretRouter = router({ value: z.string().min(1), }), ) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("secret.updated", "Secret")) .mutation(async ({ input }) => { @@ -71,6 +73,7 @@ export const secretRouter = router({ delete: protectedProcedure .input(z.object({ id: z.string(), environmentId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("EDITOR")) .use(withAudit("secret.deleted", "Secret")) .mutation(async ({ input }) => { diff --git a/src/server/routers/settings.ts b/src/server/routers/settings.ts index d5797f46..3d92728e 100644 --- a/src/server/routers/settings.ts +++ b/src/server/routers/settings.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import crypto from "crypto"; import { TRPCError } from "@trpc/server"; import { S3Client, HeadBucketCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; -import { router, protectedProcedure, requireSuperAdmin } from "@/trpc/init"; +import { router, protectedProcedure, requireSuperAdmin, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import { encrypt, decrypt } from "@/server/services/crypto"; import { withAudit } from "@/server/middleware/audit"; @@ -115,6 +115,7 @@ export const settingsRouter = router({ }), updateOidc: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -149,6 +150,7 @@ export const settingsRouter = router({ }), updateOidcRoleMapping: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -174,6 +176,7 @@ export const settingsRouter = router({ }), updateOidcTeamMappings: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ mappings: z.array(z.object({ @@ -257,6 +260,7 @@ export const settingsRouter = router({ }), updateFleet: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -282,6 +286,7 @@ export const settingsRouter = router({ }), updateAnomalyConfig: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -334,6 +339,7 @@ export const settingsRouter = router({ }), testOidc: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -403,6 +409,7 @@ export const settingsRouter = router({ // ─── Backup & Restore ───────────────────────────────────────────────────── createBackup: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .use(withAudit("settings.backup_created", "SystemSettings")) .mutation(async () => { @@ -418,6 +425,7 @@ export const settingsRouter = router({ }), previewBackup: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ filename: z.string().min(1) })) .query(async ({ input }) => { @@ -425,6 +433,7 @@ export const settingsRouter = router({ }), deleteBackup: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ filename: z.string().min(1) })) .use(withAudit("settings.backup_deleted", "SystemSettings")) @@ -434,6 +443,7 @@ export const settingsRouter = router({ }), restoreBackup: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ filename: z.string().min(1) })) .use(withAudit("settings.backup_restored", "SystemSettings")) @@ -443,6 +453,7 @@ export const settingsRouter = router({ }), updateBackupSchedule: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input( z.object({ @@ -475,6 +486,7 @@ export const settingsRouter = router({ }), testS3Connection: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ bucket: z.string().min(1), @@ -518,6 +530,7 @@ export const settingsRouter = router({ }), updateStorageBackend: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ backend: z.enum(["local", "s3"]), @@ -558,6 +571,7 @@ export const settingsRouter = router({ // ─── SCIM Provisioning ──────────────────────────────────────────────────── updateScim: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .input(z.object({ enabled: z.boolean() })) .use(withAudit("settings.scim_updated", "SystemSettings")) @@ -579,6 +593,7 @@ export const settingsRouter = router({ }), generateScimToken: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .use(withAudit("settings.scim_token_generated", "SystemSettings")) .mutation(async () => { diff --git a/src/server/routers/team.ts b/src/server/routers/team.ts index 107c2128..251cdacc 100644 --- a/src/server/routers/team.ts +++ b/src/server/routers/team.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure, withTeamAccess, requireSuperAdmin } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, requireSuperAdmin, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; import crypto from "crypto"; @@ -116,6 +116,7 @@ export const teamRouter = router({ }), create: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .use(withAudit("team.created", "Team")) .input(z.object({ name: z.string().min(1).max(100) })) @@ -133,6 +134,7 @@ export const teamRouter = router({ }), delete: protectedProcedure + .use(denyInDemo()) .use(requireSuperAdmin()) .use(withAudit("team.deleted", "Team")) .input(z.object({ teamId: z.string() })) @@ -170,6 +172,7 @@ export const teamRouter = router({ }), rename: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.renamed", "Team")) .input(z.object({ teamId: z.string(), name: z.string().min(1).max(100) })) @@ -185,6 +188,7 @@ export const teamRouter = router({ }), addMember: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_added", "Team")) .input( @@ -228,6 +232,7 @@ export const teamRouter = router({ }), removeMember: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_removed", "Team")) .input( @@ -255,6 +260,7 @@ export const teamRouter = router({ }), updateMemberRole: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_role_updated", "Team")) .input( @@ -283,6 +289,7 @@ export const teamRouter = router({ }), lockMember: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_locked", "User")) .input(z.object({ teamId: z.string(), userId: z.string() })) @@ -314,6 +321,7 @@ export const teamRouter = router({ }), unlockMember: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_unlocked", "User")) .input(z.object({ teamId: z.string(), userId: z.string() })) @@ -340,6 +348,7 @@ export const teamRouter = router({ }), resetMemberPassword: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_password_reset", "User")) .input(z.object({ teamId: z.string(), userId: z.string() })) @@ -370,6 +379,7 @@ export const teamRouter = router({ }), updateRequireTwoFactor: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.require_2fa_updated", "Team")) .input(z.object({ teamId: z.string(), requireTwoFactor: z.boolean() })) @@ -389,6 +399,7 @@ export const teamRouter = router({ { message: "Duplicate tags are not allowed" }, ), })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.updated", "Team")) .mutation(async ({ input }) => { @@ -403,6 +414,7 @@ export const teamRouter = router({ teamId: z.string(), defaultEnvironmentId: z.string().nullable(), })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.updated", "Team")) .mutation(async ({ input }) => { @@ -436,6 +448,7 @@ export const teamRouter = router({ }), linkMemberToOidc: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.member_linked_oidc", "User")) .input(z.object({ teamId: z.string(), userId: z.string() })) @@ -499,6 +512,7 @@ export const teamRouter = router({ }), updateAiConfig: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.ai_config_updated", "Team")) .input( @@ -528,6 +542,7 @@ export const teamRouter = router({ }), testAiConnection: protectedProcedure + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("team.ai_connection_tested", "Team")) .input(z.object({ teamId: z.string() })) diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index 13065b4c..315243b1 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure } from "@/trpc/init"; +import { router, protectedProcedure, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; import { withAudit } from "@/server/middleware/audit"; @@ -47,6 +47,7 @@ export const userRouter = router({ }), changePassword: protectedProcedure + .use(denyInDemo()) .use(withAudit("user.password_changed", "User")) .input( z.object({ @@ -102,6 +103,7 @@ export const userRouter = router({ .input(z.object({ name: z.string().min(1).max(100), })) + .use(denyInDemo()) .use(withAudit("user.profile_updated", "User")) .mutation(async ({ ctx, input }) => { const userId = ctx.session.user!.id!; @@ -129,6 +131,7 @@ export const userRouter = router({ * the user must verify a code via verifyAndEnableTotp first. */ setupTotp: protectedProcedure + .use(denyInDemo()) .use(withAudit("user.totp_setup_started", "User")) .mutation(async ({ ctx }) => { const userId = ctx.session.user!.id!; @@ -173,6 +176,7 @@ export const userRouter = router({ * Verify a TOTP code against the pending secret and enable 2FA. */ verifyAndEnableTotp: protectedProcedure + .use(denyInDemo()) .use(withAudit("user.totp_enabled", "User")) .input(z.object({ code: z.string().length(6) })) .mutation(async ({ ctx, input }) => { @@ -215,6 +219,7 @@ export const userRouter = router({ * Disable 2FA. Requires a valid TOTP code to confirm. */ disableTotp: protectedProcedure + .use(denyInDemo()) .use(withAudit("user.totp_disabled", "User")) .input(z.object({ code: z.string().min(6) })) .mutation(async ({ ctx, input }) => { diff --git a/src/server/routers/webhook-endpoint.ts b/src/server/routers/webhook-endpoint.ts index 763aeb98..121c5595 100644 --- a/src/server/routers/webhook-endpoint.ts +++ b/src/server/routers/webhook-endpoint.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; -import { router, protectedProcedure, withTeamAccess } from "@/trpc/init"; +import { router, protectedProcedure, withTeamAccess, denyInDemo } from "@/trpc/init"; import { prisma } from "@/lib/prisma"; import { AlertMetric } from "@/generated/prisma"; import { withAudit } from "@/server/middleware/audit"; @@ -54,6 +54,7 @@ export const webhookEndpointRouter = router({ secret: z.string().min(1).optional(), }), ) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("webhookEndpoint.created", "WebhookEndpoint")) .mutation(async ({ input }) => { @@ -95,6 +96,7 @@ export const webhookEndpointRouter = router({ secret: z.string().min(1).optional(), }), ) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("webhookEndpoint.updated", "WebhookEndpoint")) .mutation(async ({ input }) => { @@ -129,6 +131,7 @@ export const webhookEndpointRouter = router({ */ delete: protectedProcedure .input(z.object({ id: z.string(), teamId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("webhookEndpoint.deleted", "WebhookEndpoint")) .mutation(async ({ input }) => { @@ -150,6 +153,7 @@ export const webhookEndpointRouter = router({ */ toggleEnabled: protectedProcedure .input(z.object({ id: z.string(), teamId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("webhookEndpoint.toggled", "WebhookEndpoint")) .mutation(async ({ input }) => { @@ -174,6 +178,7 @@ export const webhookEndpointRouter = router({ */ testDelivery: protectedProcedure .input(z.object({ id: z.string(), teamId: z.string() })) + .use(denyInDemo()) .use(withTeamAccess("ADMIN")) .use(withAudit("webhookEndpoint.testDelivery", "WebhookEndpoint")) .mutation(async ({ input }) => {