From c4a26a692e75abf014c35ffc6ebbe12ee96c77e8 Mon Sep 17 00:00:00 2001 From: Thejas775 Date: Sat, 11 Jul 2026 13:34:53 +0530 Subject: [PATCH 1/4] =?UTF-8?q?fix(paycraft):=20production=20hardening=20?= =?UTF-8?q?=E2=80=94=20lifetime=20entitlement,=20billing=20observability,?= =?UTF-8?q?=20Next.js=20security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stripe-webhook: grant entitlement on mode=payment + async_payment_succeeded (one-time/lifetime purchases were silently dropped); reuses subscription-handler upsert path (email/current_period_end), ~100yr period for lifetime. - PayCraftBillingManager: replace 3 silent catches with PayCraftLogger.onError (transferToDevice, revokeCurrentDevice, applyPremiumResult); add 11 unit tests covering premium/cache application, logout reset, OAuth error, OTP decisioning, transfer abort guard. - dashboard: bump Next.js 14.2.15 -> 14.2.35 (resolves the 2025-12-11 critical CVE). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../paycraft/core/PayCraftBillingManager.kt | 9 +- .../core/PayCraftBillingManagerTest.kt | 310 ++++++++++++++++++ dashboard/package-lock.json | 89 +++-- dashboard/package.json | 2 +- supabase/functions/stripe-webhook/index.ts | 100 ++++++ 5 files changed, 463 insertions(+), 47 deletions(-) create mode 100644 cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt diff --git a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt index c7eaf3e..ba2ff6a 100644 --- a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt +++ b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt @@ -349,6 +349,7 @@ class PayCraftBillingManager(private val service: PayCraftService, private val s val ok = try { service.transferToDevice(token, token) } catch (e: Exception) { + PayCraftLogger.onError("transferToDevice", e.message) false } if (ok) { @@ -362,7 +363,9 @@ class PayCraftBillingManager(private val service: PayCraftService, private val s val token = DeviceTokenStore.getToken() ?: return try { service.revokeDevice(token, token) - } catch (e: Exception) { /* log */ } + } catch (e: Exception) { + PayCraftLogger.onError("revokeCurrentDevice", e.message) + } DeviceTokenStore.clearToken() store.clearCache() _billingState.value = BillingState.Free @@ -527,6 +530,10 @@ class PayCraftBillingManager(private val service: PayCraftService, private val s val sub = try { if (token != null) service.getSubscription(token) else null } catch (e: Exception) { + // Keep the premium fallback (status renders with null plan/expiry) but make + // the swallowed failure observable — otherwise a premium user silently + // renders with no plan/expiry and there is no signal in logs. + PayCraftLogger.onError("applyPremiumResult", e.message) null } val status = SubscriptionStatus( diff --git a/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt b/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt new file mode 100644 index 0000000..153ef8e --- /dev/null +++ b/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt @@ -0,0 +1,310 @@ +package com.mobilebytelabs.paycraft.core + +import com.mobilebytelabs.paycraft.model.BillingState +import com.mobilebytelabs.paycraft.model.OAuthProvider +import com.mobilebytelabs.paycraft.model.SubscriptionStatus +import com.mobilebytelabs.paycraft.network.OtpGateResult +import com.mobilebytelabs.paycraft.network.PayCraftService +import com.mobilebytelabs.paycraft.network.PremiumCheckResult +import com.mobilebytelabs.paycraft.network.RegisterDeviceResult +import com.mobilebytelabs.paycraft.network.SubscriptionDto +import com.mobilebytelabs.paycraft.persistence.PayCraftStore +import com.mobilebytelabs.paycraft.platform.currentTimeMillis +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Deterministic unit tests for [PayCraftBillingManager] — the device-conflict / + * OTP / OAuth / premium state machine. + * + * Scope note (why this is a subset of the class): the manager reaches for three + * platform singletons that are `expect object`s and therefore cannot be injected — + * [com.mobilebytelabs.paycraft.platform.DeviceTokenStore], + * [com.mobilebytelabs.paycraft.platform.PlatformInfo], and the [com.mobilebytelabs.paycraft.PayCraft] + * config object. Any code path that reads/writes the device token (register → + * conflict → OwnershipVerified → transfer/revoke, and the server-driven + * `applyPremiumResult` premium branch) depends on that filesystem/Keychain-backed + * singleton and cannot be exercised deterministically from `commonTest` (the JVM + * actual writes `~/.paycraft/device_token`; native/JS actuals differ). These tests + * therefore cover the state transitions that do not DEPEND on device-token state: + * cache-driven premium application, logout reset, the OAuth error transitions, the + * OTP verification gate, and the transfer abort guard. (One path — a correct OTP with + * no active conflict — performs a single read-only `DeviceTokenStore.getToken()`, but + * its return value cannot affect the assertion: the transition it guards requires a + * non-null conflict.) The write-driven token paths (register → conflict → + * OwnershipVerified → transfer/revoke, and the server-driven `applyPremiumResult` + * premium branch) are left to on-device / instrumented coverage. + * + * Both [PayCraftService] (network/RPC) and [PayCraftStore] (cache) are faked. + */ +class PayCraftBillingManagerTest { + + // ─── Fakes ────────────────────────────────────────────────────────────── + + /** + * Fully controllable fake of the RPC surface. Each method delegates to a + * mutable lambda so a test can inject success, failure (throw), or a specific + * return value, and can assert whether a method was invoked. + */ + private class FakePayCraftService : PayCraftService { + var transferCalled = false + var revokeCalled = false + var getSubscriptionCalled = false + + var verifyOtpBehavior: (suspend (String, String) -> Boolean) = { _, _ -> false } + var verifyOAuthBehavior: (suspend (OAuthProvider, String) -> String?) = { _, _ -> null } + var sendOtpBehavior: (suspend (String) -> Unit) = { } + var getSubscriptionBehavior: (suspend (String) -> SubscriptionDto?) = { null } + + override suspend fun isPremium(serverToken: String): Boolean = false + + override suspend fun getSubscription(serverToken: String): SubscriptionDto? { + getSubscriptionCalled = true + return getSubscriptionBehavior(serverToken) + } + + override suspend fun isTrialEligible(serverToken: String): Boolean = true + + override suspend fun registerDevice( + email: String, + platform: String, + deviceName: String, + deviceId: String, + mode: String, + ): RegisterDeviceResult = RegisterDeviceResult( + deviceToken = "tok", + conflict = false, + conflictingDeviceName = null, + conflictingLastSeen = null, + ) + + override suspend fun checkPremiumWithDevice(serverToken: String): PremiumCheckResult = + PremiumCheckResult(isPremium = false, tokenValid = true) + + override suspend fun transferToDevice(serverToken: String, newDeviceToken: String): Boolean { + transferCalled = true + return true + } + + override suspend fun revokeDevice(serverToken: String, targetToken: String): Boolean { + revokeCalled = true + return true + } + + override suspend fun checkOtpGate(): OtpGateResult = OtpGateResult(false, 0, 300) + + override suspend fun sendOtp(email: String) = sendOtpBehavior(email) + + override suspend fun verifyOtp(email: String, token: String): Boolean = + verifyOtpBehavior(email, token) + + override suspend fun verifyOAuthToken(provider: OAuthProvider, idToken: String): String? = + verifyOAuthBehavior(provider, idToken) + } + + /** In-memory [PayCraftStore] with configurable cache + email seed. */ + private class FakePayCraftStore( + private var cached: SubscriptionStatus? = null, + private var lastSynced: Long = 0L, + private var email: String? = null, + ) : PayCraftStore { + var clearCacheCalled = false + + override suspend fun saveEmail(email: String) { this.email = email } + override suspend fun getEmail(): String? = email + override suspend fun clearEmail() { email = null } + + override fun cacheSubscriptionStatus(status: SubscriptionStatus) { cached = status } + override fun getCachedSubscriptionStatus(): SubscriptionStatus? = cached + override fun getLastSyncedAt(): Long = lastSynced + override fun clearCache() { clearCacheCalled = true; cached = null } + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + /** + * A cached premium status whose expiry is far in the future with auto-renew on, + * so [SyncPolicy.syncInterval] resolves to weekly. Paired with `lastSynced = now` + * this makes [SyncPolicy.isSyncDue] return false, so the manager's async init + * block hits the "cache fresh → skip network" branch and never mutates + * [BillingState]. That gives the tests a stable, deterministic starting state. + */ + private fun freshPremiumCache() = SubscriptionStatus( + isPremium = true, + plan = "annual", + email = "user@example.com", + provider = "stripe", + expiresAt = "2999-01-01T00:00:00Z", + willRenew = true, + ) + + /** Manager seeded with a fresh premium cache — starts settled in [BillingState.Premium]. */ + private fun managerWithFreshPremiumCache(service: FakePayCraftService): PayCraftBillingManager = + PayCraftBillingManager( + service = service, + store = FakePayCraftStore( + cached = freshPremiumCache(), + lastSynced = currentTimeMillis(), + email = "user@example.com", + ), + ) + + // ─── Cache-driven premium application (applyCachedStatus) ────────────────── + + @Test + fun init_withCachedPremiumStatus_appliesPremiumStateSynchronously() { + val cached = freshPremiumCache() + val manager = PayCraftBillingManager( + service = FakePayCraftService(), + store = FakePayCraftStore(cached = cached, lastSynced = currentTimeMillis(), email = cached.email), + ) + + assertTrue(manager.isPremium.value) + assertEquals(cached, manager.subscriptionStatus.value) + val state = assertIs(manager.billingState.value) + assertEquals(cached, state.status) + // Trial state is not persisted in the cache — conservative defaults until refresh. + assertFalse(manager.isInTrial.value) + assertNull(manager.trialEndsAt.value) + } + + @Test + fun init_withCachedFreeStatus_appliesFreeState() { + val cached = SubscriptionStatus(isPremium = false, email = "user@example.com") + // Free status → daily interval; synced now → not due → async init won't overwrite. + val manager = PayCraftBillingManager( + service = FakePayCraftService(), + store = FakePayCraftStore(cached = cached, lastSynced = currentTimeMillis(), email = cached.email), + ) + + assertFalse(manager.isPremium.value) + assertEquals(BillingState.Free, manager.billingState.value) + } + + // ─── Logout reset transition ─────────────────────────────────────────────── + + @Test + fun logOut_resetsAllStateToFree() { + // No cache + no email → the async init block settles to Free without ever + // writing userEmail, so the only writer of userEmail is logOut() itself. + val store = FakePayCraftStore(cached = null, lastSynced = 0L, email = null) + val manager = PayCraftBillingManager(service = FakePayCraftService(), store = store) + + manager.logOut() + + assertFalse(manager.isPremium.value) + assertFalse(manager.isInTrial.value) + assertNull(manager.trialEndsAt.value) + assertNull(manager.userEmail.value) + assertEquals(SubscriptionStatus(), manager.subscriptionStatus.value) + assertEquals(BillingState.Free, manager.billingState.value) + assertTrue(store.clearCacheCalled) + } + + // ─── OAuth error transitions (Gate 1) ────────────────────────────────────── + + @Test + fun loginWithOAuth_serviceThrows_setsErrorStateWithMessage() = runTest { + val service = FakePayCraftService().apply { + verifyOAuthBehavior = { _, _ -> throw RuntimeException("network down") } + } + val manager = managerWithFreshPremiumCache(service) + + manager.loginWithOAuth(OAuthProvider.GOOGLE, "id-token") + + val state = assertIs(manager.billingState.value) + assertEquals("network down", state.message) + } + + @Test + fun loginWithOAuth_serviceReturnsNull_setsIdentityError() = runTest { + val service = FakePayCraftService().apply { + verifyOAuthBehavior = { _, _ -> null } + } + val manager = managerWithFreshPremiumCache(service) + + manager.loginWithOAuth(OAuthProvider.APPLE, "id-token") + + val state = assertIs(manager.billingState.value) + assertEquals("Could not verify your identity. Please try again.", state.message) + } + + // ─── OTP verification gate (Gate 2) ───────────────────────────────────────── + + @Test + fun verifyOtp_serviceSucceeds_returnsTrue() = runTest { + val service = FakePayCraftService().apply { verifyOtpBehavior = { _, _ -> true } } + val manager = managerWithFreshPremiumCache(service) + + assertTrue(manager.verifyOtp("user@example.com", "123456")) + } + + @Test + fun verifyOtp_serviceThrows_returnsFalse() = runTest { + val service = FakePayCraftService().apply { + verifyOtpBehavior = { _, _ -> throw RuntimeException("bad otp") } + } + val manager = managerWithFreshPremiumCache(service) + + assertFalse(manager.verifyOtp("user@example.com", "000000")) + } + + @Test + fun verifyOtpOwnership_noActiveConflict_returnsResultWithoutStateTransition() = runTest { + // With no cached conflict, a correct OTP must NOT flip the state to + // OwnershipVerified — that transition requires an active DeviceConflict. + val service = FakePayCraftService().apply { verifyOtpBehavior = { _, _ -> true } } + val manager = managerWithFreshPremiumCache(service) + + val ok = manager.verifyOtpOwnership("user@example.com", "123456") + + assertTrue(ok) + assertTrue( + manager.billingState.value is BillingState.Premium, + "state must not transition to OwnershipVerified without an active conflict", + ) + } + + @Test + fun verifyOtpOwnership_serviceThrows_returnsFalse() = runTest { + val service = FakePayCraftService().apply { + verifyOtpBehavior = { _, _ -> throw RuntimeException("rpc failure") } + } + val manager = managerWithFreshPremiumCache(service) + + assertFalse(manager.verifyOtpOwnership("user@example.com", "000000")) + } + + @Test + fun requestOtpVerification_serviceThrows_isSwallowed() = runTest { + val service = FakePayCraftService().apply { + sendOtpBehavior = { throw RuntimeException("send failed") } + } + val manager = managerWithFreshPremiumCache(service) + + // Must not propagate — the caller UI keeps working even if the send RPC fails. + manager.requestOtpVerification("user@example.com") + } + + // ─── Transfer abort guard ─────────────────────────────────────────────────── + + @Test + fun confirmDeviceTransfer_noOwnershipVerifiedState_isNoOp() = runTest { + val service = FakePayCraftService() + val manager = managerWithFreshPremiumCache(service) + + manager.confirmDeviceTransfer() + + // No OwnershipVerified state and no cached conflict → aborts before any RPC. + assertFalse(service.transferCalled) + assertTrue( + manager.billingState.value is BillingState.Premium, + "state must be unchanged when there is nothing to transfer", + ) + } +} diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index df650fe..3c240a5 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -12,7 +12,7 @@ "@supabase/supabase-js": "^2.45.0", "clsx": "^2.1.1", "lucide-react": "^0.400.0", - "next": "14.2.15", + "next": "14.2.35", "postmark": "^4.0.7", "razorpay": "^2.9.6", "react": "^18.3.1", @@ -1095,15 +1095,15 @@ } }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], @@ -1117,9 +1117,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], @@ -1133,9 +1133,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], @@ -1152,9 +1152,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], @@ -1171,9 +1171,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], @@ -1190,9 +1190,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], @@ -1209,9 +1209,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], @@ -1225,9 +1225,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], @@ -1241,9 +1241,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], @@ -4907,13 +4907,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.2.15", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -4928,15 +4927,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/dashboard/package.json b/dashboard/package.json index 3eb5da0..85f4913 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -14,7 +14,7 @@ "@supabase/supabase-js": "^2.45.0", "clsx": "^2.1.1", "lucide-react": "^0.400.0", - "next": "14.2.15", + "next": "14.2.35", "postmark": "^4.0.7", "razorpay": "^2.9.6", "react": "^18.3.1", diff --git a/supabase/functions/stripe-webhook/index.ts b/supabase/functions/stripe-webhook/index.ts index 154e894..c2eedac 100644 --- a/supabase/functions/stripe-webhook/index.ts +++ b/supabase/functions/stripe-webhook/index.ts @@ -202,10 +202,110 @@ serve(withWebhookRateLimit({ bucket: "webhook:stripe" }, async (req) => { tenantId, eventType: event.type, }); + } else if (session.mode === "payment" && email) { + // One-time / lifetime purchase (Product.Lifetime, tenant_products + // type="lifetime"). Stripe emits NO Subscription object for a + // payment-mode checkout, so `session.subscription` is null and the + // subscription-only branch above skipped it entirely — a lifetime + // buyer got no entitlement row and therefore no access. + // + // We synthesize an entitlement row that grants lifetime access, + // mirroring the server-side precedent in + // upi_payment_intent_mark_paid (migration 062): a "lifetime" product + // gets `current_period_end = NOW() + INTERVAL '100 years'` with + // status "active". `is_premium` (migration 016) gates purely on + // `status IN ('active','trialing') AND current_period_end > now()`, + // so a far-future period end reads as never-expiring — there is no + // NULL-means-forever path in the RPC, hence the explicit far date. + // + // Idempotency: the same shared upsert path is reused. The row is + // keyed by email (or tenant+email), so a webhook re-delivery just + // re-upserts the identical row. + + // Guard against async payment methods (e.g. some bank debits) whose + // session completes before funds settle. For those Stripe fires + // `checkout.session.async_payment_succeeded` once paid; we only grant + // on a settled payment. Synchronous card checkouts arrive "paid". + if (session.payment_status && session.payment_status !== "paid") { + console.log( + `[stripe-webhook] payment-mode session ${session.id} not yet paid (payment_status=${session.payment_status}); awaiting settlement`, + ); + break; + } + + // ~100 years out — mirrors the SQL `NOW() + INTERVAL '100 years'`. + const lifetimePeriodEnd = new Date( + Date.now() + 100 * 365 * 24 * 60 * 60 * 1000, + ); + // No Subscription object → key the entitlement on the PaymentIntent + // (unique per checkout), falling back to the Checkout Session id. + const oneTimeId = + (session.payment_intent as string) || `cs-${session.id}`; + + await handleSubscriptionEvent({ + email, + provider: "stripe", + customerId: (session.customer as string) || null, + subscriptionId: oneTimeId, + plan: session.metadata?.plan_id || "lifetime", + status: "active", + mode: stripeMode, + periodStart: new Date(), + periodEnd: lifetimePeriodEnd, + cancelAtPeriodEnd: false, + trialStart: null, + trialEnd: null, + tenantId, + eventType: event.type, + }); } break; } + case "checkout.session.async_payment_succeeded": { + // Async payment methods (e.g. certain bank debits) settle after the + // initial `checkout.session.completed`. This event confirms funds + // received. Only payment-mode (one-time/lifetime) sessions need + // handling here — subscription-mode async settlement drives entitlement + // via `invoice.paid` / `customer.subscription.updated` already. + const session = event.data.object as Stripe.Checkout.Session; + if (session.mode !== "payment") break; + + let email = + session.customer_email || (session as any).customer_details?.email; + if (!email && session.customer) { + const customer = await stripeClient.customers.retrieve( + session.customer as string, + ); + email = (customer as any).email; + } + if (!email) break; + + const lifetimePeriodEnd = new Date( + Date.now() + 100 * 365 * 24 * 60 * 60 * 1000, + ); + const oneTimeId = + (session.payment_intent as string) || `cs-${session.id}`; + + await handleSubscriptionEvent({ + email, + provider: "stripe", + customerId: (session.customer as string) || null, + subscriptionId: oneTimeId, + plan: session.metadata?.plan_id || "lifetime", + status: "active", + mode: stripeMode, + periodStart: new Date(), + periodEnd: lifetimePeriodEnd, + cancelAtPeriodEnd: false, + trialStart: null, + trialEnd: null, + tenantId, + eventType: event.type, + }); + break; + } + case "customer.subscription.updated": case "customer.subscription.deleted": { const sub = event.data.object as Stripe.Subscription; From 44424290d323a65790ea537d4ac13dd2aa083ef1 Mon Sep 17 00:00:00 2001 From: Thejas775 Date: Sat, 11 Jul 2026 13:41:52 +0530 Subject: [PATCH 2/4] =?UTF-8?q?style(paycraft):=20spotlessApply=20?= =?UTF-8?q?=E2=80=94=20fix=20import=20ordering=20(new=20test=20+=202=20pre?= =?UTF-8?q?-existing=20UI=20files)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../paycraft/ui/PayCraftBanner.kt | 2 +- .../paycraft/ui/PayCraftPaywall.kt | 2 +- .../core/PayCraftBillingManagerTest.kt | 20 +++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftBanner.kt b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftBanner.kt index 49d87f5..31bab93 100644 --- a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftBanner.kt +++ b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftBanner.kt @@ -56,7 +56,6 @@ import com.mobilebytelabs.paycraft.PayCraft import com.mobilebytelabs.paycraft.PayCraftPlatform import com.mobilebytelabs.paycraft.config.PaywallDto import com.mobilebytelabs.paycraft.core.BillingManager -import com.mobilebytelabs.paycraft.presentation.parseHexColor import com.mobilebytelabs.paycraft.generated.resources.Res import com.mobilebytelabs.paycraft.generated.resources.paycraft_banner_cta_get_premium import com.mobilebytelabs.paycraft.generated.resources.paycraft_banner_cta_manage @@ -78,6 +77,7 @@ import com.mobilebytelabs.paycraft.model.BillingBenefit import com.mobilebytelabs.paycraft.model.BillingPlan import com.mobilebytelabs.paycraft.model.BillingState import com.mobilebytelabs.paycraft.model.SubscriptionStatus +import com.mobilebytelabs.paycraft.presentation.parseHexColor import com.mobilebytelabs.paycraft.provider.StripeProvider import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource diff --git a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftPaywall.kt b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftPaywall.kt index c6aefd8..d4a1998 100644 --- a/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftPaywall.kt +++ b/cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftPaywall.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mobilebytelabs.paycraft.PayCraft import com.mobilebytelabs.paycraft.PayCraftPlatform +import com.mobilebytelabs.paycraft.config.effectiveThemeOverride import com.mobilebytelabs.paycraft.generated.resources.Res import com.mobilebytelabs.paycraft.generated.resources.paycraft_choose_plan import com.mobilebytelabs.paycraft.generated.resources.paycraft_contact_support_email @@ -57,7 +58,6 @@ import com.mobilebytelabs.paycraft.generated.resources.paycraft_error_title import com.mobilebytelabs.paycraft.generated.resources.paycraft_upgrade_plan import com.mobilebytelabs.paycraft.generated.resources.paycraft_upgrade_title import com.mobilebytelabs.paycraft.generated.resources.paycraft_your_premium_title -import com.mobilebytelabs.paycraft.config.effectiveThemeOverride import com.mobilebytelabs.paycraft.model.BillingState import com.mobilebytelabs.paycraft.presentation.Branding import com.mobilebytelabs.paycraft.presentation.PayCraftThemeProvider diff --git a/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt b/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt index 153ef8e..6621754 100644 --- a/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt +++ b/cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt @@ -100,8 +100,7 @@ class PayCraftBillingManagerTest { override suspend fun sendOtp(email: String) = sendOtpBehavior(email) - override suspend fun verifyOtp(email: String, token: String): Boolean = - verifyOtpBehavior(email, token) + override suspend fun verifyOtp(email: String, token: String): Boolean = verifyOtpBehavior(email, token) override suspend fun verifyOAuthToken(provider: OAuthProvider, idToken: String): String? = verifyOAuthBehavior(provider, idToken) @@ -115,14 +114,23 @@ class PayCraftBillingManagerTest { ) : PayCraftStore { var clearCacheCalled = false - override suspend fun saveEmail(email: String) { this.email = email } + override suspend fun saveEmail(email: String) { + this.email = email + } override suspend fun getEmail(): String? = email - override suspend fun clearEmail() { email = null } + override suspend fun clearEmail() { + email = null + } - override fun cacheSubscriptionStatus(status: SubscriptionStatus) { cached = status } + override fun cacheSubscriptionStatus(status: SubscriptionStatus) { + cached = status + } override fun getCachedSubscriptionStatus(): SubscriptionStatus? = cached override fun getLastSyncedAt(): Long = lastSynced - override fun clearCache() { clearCacheCalled = true; cached = null } + override fun clearCache() { + clearCacheCalled = true + cached = null + } } // ─── Helpers ────────────────────────────────────────────────────────────── From 4f05681bcf1d490ae987b0692610a8c246234f2a Mon Sep 17 00:00:00 2001 From: Thejas775 Date: Mon, 13 Jul 2026 20:25:05 +0530 Subject: [PATCH 3/4] fix(stripe-webhook): grant lifetime on no_payment_required (100%-off checkout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit (PR #115): a fully-discounted payment-mode checkout completes with payment_status='no_payment_required'. The prior guard skipped entitlement for anything != 'paid' and waited for async_payment_succeeded, which never fires for such sessions → the user never got lifetime access. Now grant on both 'paid' and 'no_payment_required'; only genuinely 'unpaid' async sessions await settlement. Co-Authored-By: Claude Opus 4.8 (1M context) --- supabase/functions/stripe-webhook/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/supabase/functions/stripe-webhook/index.ts b/supabase/functions/stripe-webhook/index.ts index c2eedac..27ac26b 100644 --- a/supabase/functions/stripe-webhook/index.ts +++ b/supabase/functions/stripe-webhook/index.ts @@ -225,8 +225,16 @@ serve(withWebhookRateLimit({ bucket: "webhook:stripe" }, async (req) => { // Guard against async payment methods (e.g. some bank debits) whose // session completes before funds settle. For those Stripe fires // `checkout.session.async_payment_succeeded` once paid; we only grant - // on a settled payment. Synchronous card checkouts arrive "paid". - if (session.payment_status && session.payment_status !== "paid") { + // on a settled payment. Synchronous card checkouts arrive "paid", and a + // fully-discounted (100%-off coupon) checkout settles immediately as + // "no_payment_required" — both are settled and must grant now, since + // `async_payment_succeeded` never fires for them. Only a genuinely + // "unpaid" async session should wait for settlement. + if ( + session.payment_status && + session.payment_status !== "paid" && + session.payment_status !== "no_payment_required" + ) { console.log( `[stripe-webhook] payment-mode session ${session.id} not yet paid (payment_status=${session.payment_status}); awaiting settlement`, ); From 895f8e2cebda58c03f0fa15faff4b343350b6a8e Mon Sep 17 00:00:00 2001 From: Thejas775 Date: Tue, 14 Jul 2026 17:01:16 +0530 Subject: [PATCH 4/4] fix(dashboard): sync pnpm-lock.yaml with next@14.2.35 (unblocks Vercel deploy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #114 bumped next 14.2.15→14.2.35 in package.json via npm, so pnpm-lock.yaml stayed at 14.2.15. Vercel builds with 'pnpm install --frozen-lockfile' → ERR_PNPM_OUTDATED_LOCKFILE (lockfile 14.2.15 ≠ manifest 14.2.35) → production build failed. Regenerated the lock with pnpm 10 so frozen-install matches. Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/pnpm-lock.yaml | 91 ++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 16c1541..5a06a7d 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^0.400.0 version: 0.400.0(react@18.3.1) next: - specifier: 14.2.15 - version: 14.2.15(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.35 + version: 14.2.35(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) postmark: specifier: ^4.0.7 version: 4.0.7 @@ -382,63 +382,63 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@14.2.15': - resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==} + '@next/env@14.2.35': + resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@next/swc-darwin-arm64@14.2.15': - resolution: {integrity: sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==} + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.15': - resolution: {integrity: sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==} + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.15': - resolution: {integrity: sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==} + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@14.2.15': - resolution: {integrity: sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==} + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@14.2.15': - resolution: {integrity: sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==} + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@14.2.15': - resolution: {integrity: sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==} + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@14.2.15': - resolution: {integrity: sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==} + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.15': - resolution: {integrity: sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==} + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.15': - resolution: {integrity: sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==} + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1583,10 +1583,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.15: - resolution: {integrity: sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==} + next@14.2.35: + resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} engines: {node: '>=18.17.0'} - deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -2570,33 +2569,33 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@next/env@14.2.15': {} + '@next/env@14.2.35': {} - '@next/swc-darwin-arm64@14.2.15': + '@next/swc-darwin-arm64@14.2.33': optional: true - '@next/swc-darwin-x64@14.2.15': + '@next/swc-darwin-x64@14.2.33': optional: true - '@next/swc-linux-arm64-gnu@14.2.15': + '@next/swc-linux-arm64-gnu@14.2.33': optional: true - '@next/swc-linux-arm64-musl@14.2.15': + '@next/swc-linux-arm64-musl@14.2.33': optional: true - '@next/swc-linux-x64-gnu@14.2.15': + '@next/swc-linux-x64-gnu@14.2.33': optional: true - '@next/swc-linux-x64-musl@14.2.15': + '@next/swc-linux-x64-musl@14.2.33': optional: true - '@next/swc-win32-arm64-msvc@14.2.15': + '@next/swc-win32-arm64-msvc@14.2.33': optional: true - '@next/swc-win32-ia32-msvc@14.2.15': + '@next/swc-win32-ia32-msvc@14.2.33': optional: true - '@next/swc-win32-x64-msvc@14.2.15': + '@next/swc-win32-x64-msvc@14.2.33': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3833,9 +3832,9 @@ snapshots: neo-async@2.6.2: {} - next@14.2.15(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.35(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.15 + '@next/env': 14.2.35 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001799 @@ -3845,15 +3844,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.29.7)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.15 - '@next/swc-darwin-x64': 14.2.15 - '@next/swc-linux-arm64-gnu': 14.2.15 - '@next/swc-linux-arm64-musl': 14.2.15 - '@next/swc-linux-x64-gnu': 14.2.15 - '@next/swc-linux-x64-musl': 14.2.15 - '@next/swc-win32-arm64-msvc': 14.2.15 - '@next/swc-win32-ia32-msvc': 14.2.15 - '@next/swc-win32-x64-msvc': 14.2.15 + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 '@playwright/test': 1.61.0 transitivePeerDependencies: - '@babel/core'