From a48fdf43db500b4c883b11ad789dd0c125534ad9 Mon Sep 17 00:00:00 2001 From: MaikoCode <71674307+MaikoCode@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:39:51 +0100 Subject: [PATCH 1/3] fix: verify Meta webhook signatures --- convex/http.ts | 12 ++++++ convex/meta/webhookSignature.ts | 74 +++++++++++++++++++++++++++++++++ tests/webhook-signature.test.ts | 51 +++++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 convex/meta/webhookSignature.ts create mode 100644 tests/webhook-signature.test.ts diff --git a/convex/http.ts b/convex/http.ts index 8e0411a..66a7224 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -3,6 +3,7 @@ import { httpAction } from "./_generated/server"; import { internal } from "./_generated/api"; import { auth } from "./auth"; import { requireMetaEnv } from "./meta/config"; +import { verifyMetaWebhookSignature } from "./meta/webhookSignature"; const http = httpRouter(); @@ -30,7 +31,18 @@ http.route({ path: "/meta/webhooks", method: "POST", handler: httpAction(async (ctx, request) => { + const { appSecret } = requireMetaEnv(); const body = await request.text(); + const signatureHeader = request.headers.get("x-hub-signature-256"); + + const signatureValid = await verifyMetaWebhookSignature({ + body, + appSecret, + signatureHeader, + }); + if (!signatureValid) { + return new Response("Invalid webhook signature", { status: 403 }); + } // Route based on webhook object type let isCommentWebhook = false; diff --git a/convex/meta/webhookSignature.ts b/convex/meta/webhookSignature.ts new file mode 100644 index 0000000..70ee186 --- /dev/null +++ b/convex/meta/webhookSignature.ts @@ -0,0 +1,74 @@ +const META_WEBHOOK_SIGNATURE_PREFIX = "sha256="; +const HEX_SHA256_LENGTH = 64; + +function toHex(bytes: Uint8Array) { + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +function normalizeMetaWebhookSignature(signatureHeader: string | null) { + const signature = signatureHeader?.trim() ?? ""; + if (!signature.startsWith(META_WEBHOOK_SIGNATURE_PREFIX)) { + return null; + } + + const digest = signature.slice(META_WEBHOOK_SIGNATURE_PREFIX.length); + if (digest.length !== HEX_SHA256_LENGTH || !/^[0-9a-fA-F]+$/.test(digest)) { + return null; + } + + return digest.toLowerCase(); +} + +function timingSafeHexEqual(left: string, right: string) { + let diff = left.length ^ right.length; + const length = Math.max(left.length, right.length); + + for (let index = 0; index < length; index += 1) { + const leftCode = index < left.length ? left.charCodeAt(index) : 0; + const rightCode = index < right.length ? right.charCodeAt(index) : 0; + diff |= leftCode ^ rightCode; + } + + return diff === 0; +} + +export async function computeMetaWebhookSignature(args: { + body: string; + appSecret: string; +}) { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(args.appSecret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const signature = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(args.body), + ); + + return toHex(new Uint8Array(signature)); +} + +export async function verifyMetaWebhookSignature(args: { + body: string; + appSecret: string; + signatureHeader: string | null; +}) { + const providedSignature = normalizeMetaWebhookSignature(args.signatureHeader); + if (providedSignature === null) { + return false; + } + + const expectedSignature = await computeMetaWebhookSignature({ + body: args.body, + appSecret: args.appSecret, + }); + + return timingSafeHexEqual(providedSignature, expectedSignature); +} diff --git a/tests/webhook-signature.test.ts b/tests/webhook-signature.test.ts new file mode 100644 index 0000000..c5024a8 --- /dev/null +++ b/tests/webhook-signature.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + computeMetaWebhookSignature, + verifyMetaWebhookSignature, +} from "@/convex/meta/webhookSignature"; + +describe("Meta webhook signature verification", () => { + it("accepts a valid x-hub-signature-256 header", async () => { + const body = JSON.stringify({ object: "instagram", entry: [] }); + const appSecret = "meta-app-secret"; + const digest = await computeMetaWebhookSignature({ body, appSecret }); + + await expect( + verifyMetaWebhookSignature({ + body, + appSecret, + signatureHeader: `sha256=${digest}`, + }), + ).resolves.toBe(true); + }); + + it("rejects missing, malformed, or tampered signatures", async () => { + const body = JSON.stringify({ object: "instagram", entry: [] }); + const appSecret = "meta-app-secret"; + const digest = await computeMetaWebhookSignature({ body, appSecret }); + + await expect( + verifyMetaWebhookSignature({ + body, + appSecret, + signatureHeader: null, + }), + ).resolves.toBe(false); + + await expect( + verifyMetaWebhookSignature({ + body, + appSecret, + signatureHeader: `sha1=${digest}`, + }), + ).resolves.toBe(false); + + await expect( + verifyMetaWebhookSignature({ + body: body.replace("instagram", "tampered"), + appSecret, + signatureHeader: `sha256=${digest}`, + }), + ).resolves.toBe(false); + }); +}); From 82bb57a7699e9610fe3ceccc2b5dabc39dbc896d Mon Sep 17 00:00:00 2001 From: MaikoCode <71674307+MaikoCode@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:40:07 +0100 Subject: [PATCH 2/3] fix: harden Meta OAuth session handling --- convex/accounts.ts | 22 ++- convex/meta/config.ts | 8 +- convex/meta/oauth.ts | 3 +- lib/meta.ts | 12 +- tests/meta-oauth-session.test.ts | 269 +++++++++++++++++++++++++++++++ tests/meta-site-url.test.ts | 52 ++++++ 6 files changed, 359 insertions(+), 7 deletions(-) create mode 100644 tests/meta-oauth-session.test.ts create mode 100644 tests/meta-site-url.test.ts diff --git a/convex/accounts.ts b/convex/accounts.ts index 3061bd9..8915eae 100644 --- a/convex/accounts.ts +++ b/convex/accounts.ts @@ -651,12 +651,27 @@ export const emergencyReconnectAccount = internalMutation({ }); export const getConnectSessionByState = internalQuery({ - args: { state: v.string() }, + args: { state: v.string(), redirectUri: v.string() }, handler: async (ctx, args) => { - return await ctx.db + const userId = await requireCurrentUserId(ctx); + const workspace = await requireCurrentWorkspace(ctx); + const session = await ctx.db .query("instagramConnectSessions") .withIndex("by_state", (q) => q.eq("state", args.state)) .unique(); + + if ( + session === null || + session.createdByUserId !== userId || + session.workspaceId !== workspace._id || + session.status !== "pending" || + session.expiresAt < Date.now() || + session.redirectUri !== args.redirectUri + ) { + return null; + } + + return session; }, }); @@ -698,6 +713,9 @@ export const upsertConnectedAccount = internalMutation({ if (session === null) { throw new Error("Connection session not found."); } + if (session.status !== "pending" || session.expiresAt < Date.now()) { + throw new Error("Connection session is not pending."); + } const existingAccount = await getWorkspaceInstagramAccountByExternalId( ctx, diff --git a/convex/meta/config.ts b/convex/meta/config.ts index 981144d..0b198c0 100644 --- a/convex/meta/config.ts +++ b/convex/meta/config.ts @@ -31,10 +31,14 @@ export function requireMetaEnv() { } export function requireSiteUrl() { - const siteUrl = process.env.SITE_URL; + const siteUrl = process.env.SITE_URL?.trim(); if (!siteUrl) { throw new Error("Missing SITE_URL. Configure SITE_URL for tracked links."); } + const parsed = new URL(siteUrl); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("SITE_URL must use http or https."); + } - return siteUrl; + return parsed.origin; } diff --git a/convex/meta/oauth.ts b/convex/meta/oauth.ts index b4b672a..c44c2ca 100644 --- a/convex/meta/oauth.ts +++ b/convex/meta/oauth.ts @@ -258,10 +258,11 @@ export const exchangeCodeForAccount = action({ internal.accounts.getConnectSessionByState, { state: args.state, + redirectUri: args.redirectUri, }, ); - if (session === null || session.expiresAt < Date.now()) { + if (session === null) { throw new Error( "This Instagram connection session is invalid or expired.", ); diff --git a/lib/meta.ts b/lib/meta.ts index 020736f..b54915b 100644 --- a/lib/meta.ts +++ b/lib/meta.ts @@ -13,9 +13,17 @@ export function isMetaConfigured() { } export function getSiteUrl(request: Request) { - const siteUrl = process.env.SITE_URL; + const siteUrl = process.env.SITE_URL?.trim(); if (siteUrl) { - return siteUrl; + const parsed = new URL(siteUrl); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("SITE_URL must use http or https."); + } + return parsed.origin; + } + + if (process.env.NODE_ENV !== "development" && process.env.NODE_ENV !== "test") { + throw new Error("SITE_URL is required outside local development."); } return new URL(request.url).origin; diff --git a/tests/meta-oauth-session.test.ts b/tests/meta-oauth-session.test.ts new file mode 100644 index 0000000..996c193 --- /dev/null +++ b/tests/meta-oauth-session.test.ts @@ -0,0 +1,269 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { convexTest } from "convex-test"; +import { api, internal } from "@/convex/_generated/api"; +import type { Id } from "@/convex/_generated/dataModel"; +import schema from "@/convex/schema"; +import { modules } from "@/convex/test.setup"; + +const BASE_TIME = new Date("2026-04-15T10:00:00.000Z").getTime(); +const REDIRECT_URI = "https://app.example.com/api/meta/callback"; +const ORIGINAL_META_APP_ID = process.env.META_APP_ID; +const ORIGINAL_META_APP_SECRET = process.env.META_APP_SECRET; +const ORIGINAL_META_VERIFY_TOKEN = process.env.META_VERIFY_TOKEN; + +type Fixture = { + userId: Id<"users">; + workspaceId: Id<"workspaces">; +}; + +async function seedOperator( + t: ReturnType, + args: { email: string; name: string }, +): Promise { + return await t.run(async (ctx) => { + const userId = await ctx.db.insert("users", { + name: args.name, + email: args.email, + }); + const workspaceId = await ctx.db.insert("workspaces", { + ownerUserId: userId, + name: `${args.name} Workspace`, + timezone: "UTC", + }); + + return { userId, workspaceId }; + }); +} + +async function insertConnectSession( + t: ReturnType, + fixture: Fixture, + args: { + state: string; + status?: "pending" | "completed" | "failed"; + redirectUri?: string; + expiresAt?: number; + }, +) { + await t.run(async (ctx) => { + await ctx.db.insert("instagramConnectSessions", { + workspaceId: fixture.workspaceId, + createdByUserId: fixture.userId, + state: args.state, + redirectUri: args.redirectUri ?? REDIRECT_URI, + requestedScopes: ["instagram_business_basic"], + status: args.status ?? "pending", + expiresAt: args.expiresAt ?? BASE_TIME + 15 * 60 * 1000, + errorMessage: null, + }); + }); +} + +function upsertAccountArgs(state: string) { + return { + state, + instagramAccountId: `ig_${state}`, + metaUserId: null, + username: "nudgra", + name: "Nudgra", + profilePictureUrl: null, + accountType: "business" as const, + graphAccessToken: "graph-token", + tokenExpiresAt: BASE_TIME + 60 * 24 * 60 * 60 * 1000, + scopes: ["instagram_business_basic"], + webhookSubscriptionStatus: "active" as const, + status: "connected" as const, + lastError: null, + graphApiVersion: "v23.0", + }; +} + +function restoreOptionalEnv(key: string, value: string | undefined) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + +function mockMetaOAuthFetch() { + return vi.fn(async (input: RequestInfo | URL) => { + const url = input.toString(); + + if (url === "https://api.instagram.com/oauth/access_token") { + return new Response( + JSON.stringify({ + access_token: "short-lived-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (url.startsWith("https://graph.instagram.com/access_token")) { + return new Response( + JSON.stringify({ + access_token: "long-lived-token", + expires_in: 60 * 24 * 60 * 60, + }), + { status: 200 }, + ); + } + + if (url.startsWith("https://graph.instagram.com/v23.0/me")) { + return new Response( + JSON.stringify({ + user_id: "ig_connected", + username: "nudgra", + name: "Nudgra", + account_type: "BUSINESS", + profile_picture_url: "https://example.com/profile.jpg", + }), + { status: 200 }, + ); + } + + if ( + url.startsWith( + "https://graph.instagram.com/v23.0/ig_connected/subscribed_apps", + ) + ) { + return new Response("{}", { status: 200 }); + } + + throw new Error(`Unexpected Meta request: ${url}`); + }); +} + +describe("Meta OAuth connect session guards", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(BASE_TIME); + process.env.NUDGRA_ALLOWED_EMAILS = "owner@example.com,other@example.com"; + process.env.META_APP_ID = "meta-app-id"; + process.env.META_APP_SECRET = "meta-app-secret"; + process.env.META_VERIFY_TOKEN = "meta-verify-token"; + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + process.env.NUDGRA_ALLOWED_EMAILS = "operator@example.com,test@example.com"; + restoreOptionalEnv("META_APP_ID", ORIGINAL_META_APP_ID); + restoreOptionalEnv("META_APP_SECRET", ORIGINAL_META_APP_SECRET); + restoreOptionalEnv("META_VERIFY_TOKEN", ORIGINAL_META_VERIFY_TOKEN); + }); + + it("returns a pending connect session only to the creating operator", async () => { + const t = convexTest({ schema, modules }); + const owner = await seedOperator(t, { + email: "owner@example.com", + name: "Owner", + }); + const other = await seedOperator(t, { + email: "other@example.com", + name: "Other", + }); + await insertConnectSession(t, owner, { state: "state-owner" }); + + const ownerT = t.withIdentity({ subject: owner.userId }); + const otherT = t.withIdentity({ subject: other.userId }); + + const ownerSession = await ownerT.query( + internal.accounts.getConnectSessionByState, + { + state: "state-owner", + redirectUri: REDIRECT_URI, + }, + ); + const otherSession = await otherT.query( + internal.accounts.getConnectSessionByState, + { + state: "state-owner", + redirectUri: REDIRECT_URI, + }, + ); + const wrongRedirectSession = await ownerT.query( + internal.accounts.getConnectSessionByState, + { + state: "state-owner", + redirectUri: "https://app.example.com/wrong", + }, + ); + + expect(ownerSession?.createdByUserId).toBe(owner.userId); + expect(otherSession).toBeNull(); + expect(wrongRedirectSession).toBeNull(); + }); + + it("rejects completed or expired sessions before account upsert", async () => { + const t = convexTest({ schema, modules }); + const owner = await seedOperator(t, { + email: "owner@example.com", + name: "Owner", + }); + + await insertConnectSession(t, owner, { + state: "completed-state", + status: "completed", + }); + await insertConnectSession(t, owner, { + state: "expired-state", + expiresAt: BASE_TIME - 1, + }); + + await expect( + t.mutation( + internal.accounts.upsertConnectedAccount, + upsertAccountArgs("completed-state"), + ), + ).rejects.toThrow("not pending"); + await expect( + t.mutation( + internal.accounts.upsertConnectedAccount, + upsertAccountArgs("expired-state"), + ), + ).rejects.toThrow("not pending"); + }); + + it("allows the creating operator to complete the public OAuth action once", async () => { + const t = convexTest({ schema, modules }); + const owner = await seedOperator(t, { + email: "owner@example.com", + name: "Owner", + }); + await insertConnectSession(t, owner, { state: "action-state" }); + vi.stubGlobal("fetch", mockMetaOAuthFetch()); + + const ownerT = t.withIdentity({ subject: owner.userId }); + const result = await ownerT.action(api.meta.oauth.exchangeCodeForAccount, { + code: "authorization-code", + state: "action-state", + redirectUri: REDIRECT_URI, + }); + + expect(result).toMatchObject({ + accountId: "ig_connected", + username: "nudgra", + warning: null, + }); + + const account = await t.run((ctx) => + ctx.db + .query("instagramAccounts") + .withIndex("by_instagram_account_id", (q) => + q.eq("instagramAccountId", "ig_connected"), + ) + .unique(), + ); + expect(account?.graphAccessToken).toBe("long-lived-token"); + + await expect( + ownerT.action(api.meta.oauth.exchangeCodeForAccount, { + code: "authorization-code", + state: "action-state", + redirectUri: REDIRECT_URI, + }), + ).rejects.toThrow("invalid or expired"); + }); +}); diff --git a/tests/meta-site-url.test.ts b/tests/meta-site-url.test.ts new file mode 100644 index 0000000..0f06324 --- /dev/null +++ b/tests/meta-site-url.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { getSiteUrl } from "@/lib/meta"; + +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; +const ORIGINAL_SITE_URL = process.env.SITE_URL; + +function restoreEnv() { + if (ORIGINAL_NODE_ENV === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = ORIGINAL_NODE_ENV; + } + + if (ORIGINAL_SITE_URL === undefined) { + delete process.env.SITE_URL; + } else { + process.env.SITE_URL = ORIGINAL_SITE_URL; + } +} + +describe("Meta site URL configuration", () => { + afterEach(() => { + restoreEnv(); + }); + + it("uses the configured SITE_URL origin", () => { + process.env.NODE_ENV = "production"; + process.env.SITE_URL = "https://app.example.com/some/path"; + + expect(getSiteUrl(new Request("https://spoofed.example.com"))).toBe( + "https://app.example.com", + ); + }); + + it("requires SITE_URL outside development and test", () => { + process.env.NODE_ENV = "production"; + delete process.env.SITE_URL; + + expect(() => + getSiteUrl(new Request("https://spoofed.example.com")), + ).toThrow("SITE_URL is required"); + }); + + it("allows request-origin fallback in test mode", () => { + process.env.NODE_ENV = "test"; + delete process.env.SITE_URL; + + expect(getSiteUrl(new Request("https://local.example.com/path"))).toBe( + "https://local.example.com", + ); + }); +}); From e995058d3bf828f05052e60ea2a1050dc5e7d9e1 Mon Sep 17 00:00:00 2001 From: MaikoCode <71674307+MaikoCode@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:40:22 +0100 Subject: [PATCH 3/3] chore: upgrade audited dependencies --- package-lock.json | 100 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46ff3d4..b4b16a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "clsx": "^2.1.1", "convex": "^1.36.1", "lucide-react": "^1.11.0", - "next": "16.2.4", + "next": "16.2.7", "radix-ui": "^1.4.3", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -1815,9 +1815,9 @@ } }, "node_modules/@next/env": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", - "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz", + "integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1831,9 +1831,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz", + "integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==", "cpu": [ "arm64" ], @@ -1847,9 +1847,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz", + "integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==", "cpu": [ "x64" ], @@ -1863,9 +1863,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz", + "integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==", "cpu": [ "arm64" ], @@ -1879,9 +1879,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz", + "integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==", "cpu": [ "arm64" ], @@ -1895,9 +1895,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz", + "integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==", "cpu": [ "x64" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz", + "integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==", "cpu": [ "x64" ], @@ -1927,9 +1927,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz", + "integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==", "cpu": [ "arm64" ], @@ -1943,9 +1943,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz", + "integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==", "cpu": [ "x64" ], @@ -5247,9 +5247,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -5577,14 +5577,14 @@ "license": "MIT" }, "node_modules/convex": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.36.1.tgz", - "integrity": "sha512-NVnwNqU+h8jyPuS0Itvj4MPH9c2yF+tA/RNoSDpCqiLhmYD4+kZxm0dDkVM0QDzz66wem9NqheBb9YQGsHwzBQ==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.40.0.tgz", + "integrity": "sha512-jChWEB45q+9Ibryc7hg0l6hB1xA4zwE2y6ZhkhGP6oJkqYeiURkMagA2ZQZYMy1/T8PZ9ztoVJJtbL/+Ob851Q==", "license": "Apache-2.0", "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", - "ws": "8.18.0" + "ws": "8.20.1" }, "bin": { "convex": "bin/main.js" @@ -8412,12 +8412,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", - "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz", + "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==", "license": "MIT", "dependencies": { - "@next/env": "16.2.4", + "@next/env": "16.2.7", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -8431,14 +8431,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.4", - "@next/swc-darwin-x64": "16.2.4", - "@next/swc-linux-arm64-gnu": "16.2.4", - "@next/swc-linux-arm64-musl": "16.2.4", - "@next/swc-linux-x64-gnu": "16.2.4", - "@next/swc-linux-x64-musl": "16.2.4", - "@next/swc-win32-arm64-msvc": "16.2.4", - "@next/swc-win32-x64-msvc": "16.2.4", + "@next/swc-darwin-arm64": "16.2.7", + "@next/swc-darwin-x64": "16.2.7", + "@next/swc-linux-arm64-gnu": "16.2.7", + "@next/swc-linux-arm64-musl": "16.2.7", + "@next/swc-linux-x64-gnu": "16.2.7", + "@next/swc-linux-x64-musl": "16.2.7", + "@next/swc-win32-arm64-msvc": "16.2.7", + "@next/swc-win32-x64-msvc": "16.2.7", "sharp": "^0.34.5" }, "peerDependencies": { @@ -10889,9 +10889,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index acd074f..23ecb43 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "clsx": "^2.1.1", "convex": "^1.36.1", "lucide-react": "^1.11.0", - "next": "16.2.4", + "next": "16.2.7", "radix-ui": "^1.4.3", "react": "^19.2.5", "react-dom": "^19.2.5",