From e6a6998285291ea8703af922354a43b467a44bbd Mon Sep 17 00:00:00 2001
From: Taleef7
Date: Tue, 7 Apr 2026 21:17:37 -0400
Subject: [PATCH 1/3] fix: harden lifecycle flows and local verification
---
.gitignore | 4 +-
app/api/cron/expire-packages/route.test.ts | 45 +
app/api/cron/expire-packages/route.ts | 3 +-
app/dashboard/packages/[id]/page.tsx | 26 +-
app/dashboard/packages/actions.ts | 114 +-
app/dashboard/packages/new/page.tsx | 123 +-
app/page.tsx | 36 +-
app/tutor/sessions/page.tsx | 5 +-
components/dashboards/SessionCompleteForm.tsx | 9 +-
eslint.config.mjs | 4 +
.../session-status-transitions.test.ts | 47 +
lib/services/session-status-transitions.ts | 31 +
lib/services/sessions.ts | 39 +-
lib/supabase/database.types.ts | 13 +
package-lock.json | 1600 +++++++----------
package.json | 11 +-
middleware.ts => proxy.ts | 2 +-
scripts/with-local-supabase-env.mjs | 61 +
.../__tests__/lifecycle-hardening.test.ts | 33 +
.../lifecycle-rls.integration.test.ts | 131 ++
...0001_harden_lifecycle_rls_and_checkout.sql | 298 +++
vitest.config.ts | 2 +-
22 files changed, 1547 insertions(+), 1090 deletions(-)
create mode 100644 app/api/cron/expire-packages/route.test.ts
create mode 100644 lib/services/__tests__/session-status-transitions.test.ts
create mode 100644 lib/services/session-status-transitions.ts
rename middleware.ts => proxy.ts (98%)
create mode 100644 scripts/with-local-supabase-env.mjs
create mode 100644 supabase/__tests__/lifecycle-hardening.test.ts
create mode 100644 supabase/__tests__/lifecycle-rls.integration.test.ts
create mode 100644 supabase/migrations/20260408000001_harden_lifecycle_rls_and_checkout.sql
diff --git a/.gitignore b/.gitignore
index 24d87d8..c377feb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -143,6 +143,8 @@ vite.config.ts.timestamp-*
# Claude Code local tool state
.claude/
+.opencode/
+.tmp/
# MCP server state directories
.playwright-mcp/
@@ -153,4 +155,4 @@ vite.config.ts.timestamp-*
# Playwright test output (generated)
playwright-report/
-test-results/
\ No newline at end of file
+test-results/
diff --git a/app/api/cron/expire-packages/route.test.ts b/app/api/cron/expire-packages/route.test.ts
new file mode 100644
index 0000000..6a68516
--- /dev/null
+++ b/app/api/cron/expire-packages/route.test.ts
@@ -0,0 +1,45 @@
+import { beforeEach, describe, expect, test, vi } from 'vitest'
+
+vi.mock('@/lib/services/payments', () => ({
+ expirePackages: vi.fn(async () => 0),
+}))
+
+import { expirePackages } from '@/lib/services/payments'
+import { GET } from './route'
+
+const mockedExpirePackages = vi.mocked(expirePackages)
+
+describe('GET /api/cron/expire-packages', () => {
+ beforeEach(() => {
+ vi.unstubAllEnvs()
+ mockedExpirePackages.mockClear()
+ })
+
+ test('fails closed when CRON_SECRET is missing', async () => {
+ vi.stubEnv('CRON_SECRET', undefined)
+
+ const response = await GET(
+ new Request('http://localhost/api/cron/expire-packages', {
+ headers: { authorization: 'Bearer undefined' },
+ }),
+ )
+
+ expect(response.status).toBe(401)
+ expect(mockedExpirePackages).not.toHaveBeenCalled()
+ })
+
+ test('runs expiration when the configured bearer token matches', async () => {
+ vi.stubEnv('CRON_SECRET', 'local-cron-secret')
+
+ const response = await GET(
+ new Request('http://localhost/api/cron/expire-packages', {
+ headers: { authorization: 'Bearer local-cron-secret' },
+ }),
+ )
+ const body = await response.json()
+
+ expect(response.status).toBe(200)
+ expect(body).toEqual({ ok: true, expired: 0 })
+ expect(mockedExpirePackages).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/app/api/cron/expire-packages/route.ts b/app/api/cron/expire-packages/route.ts
index ea5f6b2..a3ba4dd 100644
--- a/app/api/cron/expire-packages/route.ts
+++ b/app/api/cron/expire-packages/route.ts
@@ -3,8 +3,9 @@ import { expirePackages } from "@/lib/services/payments";
export async function GET(request: Request) {
const authHeader = request.headers.get("authorization");
+ const cronSecret = process.env.CRON_SECRET;
- if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
+ if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
diff --git a/app/dashboard/packages/[id]/page.tsx b/app/dashboard/packages/[id]/page.tsx
index f877c93..e88e418 100644
--- a/app/dashboard/packages/[id]/page.tsx
+++ b/app/dashboard/packages/[id]/page.tsx
@@ -174,6 +174,9 @@ export default function PackageDetailPage() {
newProofPath
)
if (!result.success) {
+ if (file && newProofPath && newProofPath !== payment.proof_path) {
+ await supabase.storage.from('payment-proofs').remove([newProofPath])
+ }
setError(result.error || 'Failed to resubmit payment.')
setSubmitting(false)
return
@@ -221,17 +224,18 @@ export default function PackageDetailPage() {
proofPath = uploadData.path
}
- // Update payment with reference and/or proof
- const { error: updateError } = await supabase
- .from('payments')
- .update({
- reference: reference.trim() || null,
- proof_path: proofPath,
- })
- .eq('id', payment.id)
-
- if (updateError) {
- setError('Failed to save payment details. Please try again.')
+ const { updatePendingPaymentDetails } = await import('@/app/dashboard/packages/actions')
+ const result = await updatePendingPaymentDetails(
+ payment.id,
+ reference.trim() || null,
+ proofPath,
+ )
+
+ if (!result.success) {
+ if (file && proofPath && proofPath !== payment.proof_path) {
+ await supabase.storage.from('payment-proofs').remove([proofPath])
+ }
+ setError(result.error || 'Failed to save payment details. Please try again.')
setSubmitting(false)
return
}
diff --git a/app/dashboard/packages/actions.ts b/app/dashboard/packages/actions.ts
index dd39acf..ff4aec7 100644
--- a/app/dashboard/packages/actions.ts
+++ b/app/dashboard/packages/actions.ts
@@ -1,7 +1,14 @@
'use server'
+import { revalidatePath } from 'next/cache'
+import { PACKAGES, type PackageTier } from '@/lib/config/pricing'
+import { createAdminClient } from '@/lib/supabase/admin'
import { createClient } from '@/lib/supabase/server'
+function getPackageTier(tier: number): PackageTier | null {
+ return PACKAGES.some((pkg) => pkg.tier === tier) ? (tier as PackageTier) : null
+}
+
/**
* Generate a signed URL for a payment proof file.
* Only the owner of the payment can request this.
@@ -41,6 +48,96 @@ export async function getPaymentProofSignedUrl(
return { url: data?.signedUrl ?? null, error: null }
}
+export async function checkoutPackage(
+ requestId: string,
+ tier: number,
+): Promise<{ packageId: string | null; error: string | null }> {
+ const selectedTier = getPackageTier(tier)
+ if (!selectedTier) {
+ return { packageId: null, error: 'Invalid package selection.' }
+ }
+
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) {
+ return { packageId: null, error: 'Not authenticated' }
+ }
+
+ const { data, error } = await supabase.rpc('checkout_package', {
+ p_request_id: requestId,
+ p_tier_sessions: selectedTier,
+ })
+
+ if (error || !data) {
+ return {
+ packageId: null,
+ error: error?.message ?? 'Failed to create package. Please try again.',
+ }
+ }
+
+ revalidatePath('/dashboard')
+ revalidatePath('/dashboard/requests')
+ revalidatePath(`/dashboard/packages/${data}`)
+
+ return { packageId: data, error: null }
+}
+
+export async function updatePendingPaymentDetails(
+ paymentId: string,
+ reference: string | null,
+ proofPath: string | null,
+): Promise<{ success: boolean; error: string | null }> {
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) {
+ return { success: false, error: 'Not authenticated' }
+ }
+
+ const { data: payment } = await supabase
+ .from('payments')
+ .select('id, package_id, payer_user_id, status')
+ .eq('id', paymentId)
+ .single()
+
+ if (!payment || payment.payer_user_id !== user.id) {
+ return { success: false, error: 'Unauthorized' }
+ }
+
+ if (payment.status !== 'pending') {
+ return { success: false, error: 'Payment is not in pending status' }
+ }
+
+ const admin = createAdminClient()
+ const { data: updated, error } = await admin
+ .from('payments')
+ .update({
+ reference: reference?.trim() || null,
+ proof_path: proofPath,
+ rejection_note: null,
+ verified_by_user_id: null,
+ verified_at: null,
+ })
+ .eq('id', paymentId)
+ .eq('payer_user_id', user.id)
+ .eq('status', 'pending')
+ .select('id, package_id')
+
+ if (error || !updated || updated.length === 0) {
+ return { success: false, error: 'Failed to save payment details. Please try again.' }
+ }
+
+ revalidatePath(`/dashboard/packages/${payment.package_id}`)
+ revalidatePath('/admin/payments')
+
+ return { success: true, error: null }
+}
+
/**
* Resubmit a rejected payment β resets status to pending.
*/
@@ -73,21 +170,32 @@ export async function resubmitRejectedPayment(
return { success: false, error: 'Payment is not in rejected status' }
}
- // Reset payment to pending with new proof/reference
- const { error } = await supabase
+ const admin = createAdminClient()
+
+ // Reset payment to pending with new proof/reference. The admin client is used
+ // after ownership validation because the payer RLS update policy only permits
+ // edits to already-pending rows.
+ const { data: updated, error } = await admin
.from('payments')
.update({
status: 'pending',
reference: reference?.trim() || null,
proof_path: proofPath,
+ rejection_note: null,
verified_by_user_id: null,
verified_at: null,
})
.eq('id', paymentId)
+ .eq('payer_user_id', user.id)
+ .eq('status', 'rejected')
+ .select('id, package_id')
- if (error) {
+ if (error || !updated || updated.length === 0) {
return { success: false, error: 'Failed to resubmit payment. Please try again.' }
}
+ revalidatePath(`/dashboard/packages/${updated[0].package_id}`)
+ revalidatePath('/admin/payments')
+
return { success: true, error: null }
}
diff --git a/app/dashboard/packages/new/page.tsx b/app/dashboard/packages/new/page.tsx
index 9641bd5..a06d791 100644
--- a/app/dashboard/packages/new/page.tsx
+++ b/app/dashboard/packages/new/page.tsx
@@ -1,11 +1,10 @@
-// E5 S5.1 T5.3: Package selection page β student selects 8/12/20 session tier
+// E5 S5.1 T5.3: Package selection page - student selects 8/12/20 session tier
// Closes #31 #35
'use client'
import { Suspense, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
-import { createClient } from '@/lib/supabase/client'
import { PACKAGES } from '@/lib/config/pricing'
function NewPackageContent() {
@@ -15,7 +14,7 @@ function NewPackageContent() {
const tierParam = searchParams.get('tier')
const [selected, setSelected] = useState(
- tierParam ? Number(tierParam) : null
+ tierParam ? Number(tierParam) : null,
)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
@@ -33,16 +32,6 @@ function NewPackageContent() {
setLoading(true)
setError(null)
- const supabase = createClient()
- const {
- data: { user },
- } = await supabase.auth.getUser()
-
- if (!user) {
- router.push('/auth/sign-in')
- return
- }
-
const pkg = PACKAGES.find((p) => p.tier === selected)
if (!pkg) {
setError('Invalid package selection.')
@@ -50,90 +39,20 @@ function NewPackageContent() {
return
}
- const today = new Date()
- const startDate = today.toISOString().slice(0, 10)
- // Compute end date as same day next month, clamped to last day of that month
- const endDateObj = new Date(
- Date.UTC(today.getUTCFullYear(), today.getUTCMonth() + 1, today.getUTCDate()),
- )
- // If day overflowed into the following month (e.g. Jan 31 β Mar), clamp to last day
- if (endDateObj.getUTCMonth() !== (today.getUTCMonth() + 1) % 12) {
- endDateObj.setUTCDate(0)
- }
- const endDate = endDateObj.toISOString().slice(0, 10)
-
- // Check for existing active/pending package to prevent duplicates
- const { data: existingPkg } = await supabase
- .from('packages')
- .select('id')
- .eq('request_id', requestId)
- .in('status', ['pending', 'active'])
- .maybeSingle()
-
- if (existingPkg) {
- router.push(`/dashboard/packages/${existingPkg.id}`)
- return
- }
+ const { checkoutPackage } = await import('@/app/dashboard/packages/actions')
+ const result = await checkoutPackage(requestId, pkg.tier)
- // Insert package row
- const { data: newPkg, error: pkgError } = await supabase
- .from('packages')
- .insert([
- {
- request_id: requestId,
- tier_sessions: pkg.tier,
- start_date: startDate,
- end_date: endDate,
- sessions_total: pkg.tier,
- sessions_used: 0,
- status: 'pending',
- },
- ])
- .select()
- .single()
-
- if (pkgError || !newPkg) {
- setError('Failed to create package. Please try again.')
- setLoading(false)
- return
- }
-
- // Advance request status to payment_pending
- const { data: updatedRequests, error: reqError } = await supabase
- .from('requests')
- .update({ status: 'payment_pending' })
- .eq('id', requestId)
- .eq('status', 'new')
- .select()
-
- if (reqError || !updatedRequests || updatedRequests.length === 0) {
- setError('Package created but failed to update request status. Please contact support.')
- setLoading(false)
- return
- }
-
- // Create initial payment row
- const { data: payment, error: payError } = await supabase
- .from('payments')
- .insert([
- {
- package_id: newPkg.id,
- payer_user_id: user.id,
- amount_pkr: pkg.pricePerMonthPkr,
- method: 'bank_transfer',
- status: 'pending',
- },
- ])
- .select()
- .single()
-
- if (payError || !payment) {
- setError('Package created but failed to create payment record. Please contact support.')
+ if (!result.packageId) {
+ if (result.error === 'Not authenticated') {
+ router.push('/auth/sign-in')
+ return
+ }
+ setError(result.error ?? 'Failed to create package. Please try again.')
setLoading(false)
return
}
- router.push(`/dashboard/packages/${newPkg.id}`)
+ router.push(`/dashboard/packages/${result.packageId}`)
}
return (
@@ -154,7 +73,6 @@ function NewPackageContent() {
)}
- {/* Package cards */}
{PACKAGES.map((pkg) => {
const isSelected = selected === pkg.tier
@@ -172,11 +90,15 @@ function NewPackageContent() {
{pkg.sessionsPerMonth} Sessions
-
{pkg.typicalFrequency}
+
+ {pkg.typicalFrequency}
+
PKR {pkg.pricePerMonthPkr.toLocaleString()}
-
{pkg.description}
+
+ {pkg.description}
+
{isSelected && (
✓ Selected
@@ -187,11 +109,10 @@ function NewPackageContent() {
})}
- {/* Policy notes */}
-
π¦ Packages are per subject β one package covers one subject for the month.
-
β οΈ Unused sessions do not carry over to the next month.
-
π All sessions are 60 minutes, one-to-one, online via Google Meet.
+
Packages are per subject - one package covers one subject for the month.
+
Unused sessions do not carry over to the next month.
+
All sessions are 60 minutes, one-to-one, online via Google Meet.
- {loading ? 'Creating packageβ¦' : 'Continue to payment β'}
+ {loading ? 'Creating package...' : 'Continue to payment ->'}
@@ -212,7 +133,7 @@ export default function NewPackagePage() {
- Loadingβ¦
+ Loading...
}
>
diff --git a/app/page.tsx b/app/page.tsx
index 30c5804..00654d9 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,9 @@
import Link from 'next/link'
+import { LeadForm } from '@/components/LeadForm'
import { WhatsAppCTA } from '@/components/WhatsAppCTA'
import { createClient } from '@/lib/supabase/server'
import { signOut } from '@/app/auth/actions'
+import { PACKAGES as PACKAGE_CONFIGS } from '@/lib/config/pricing'
const SUBJECTS = [
'Mathematics',
@@ -15,7 +17,7 @@ const SUBJECTS = [
'Urdu',
]
-const PACKAGES = [
+const PACKAGE_CARD_STYLES = [
{
sessions: 8,
frequency: '~2Γ per week',
@@ -45,6 +47,21 @@ const PACKAGES = [
},
]
+const LANDING_PACKAGES = PACKAGE_CONFIGS.map((pkg) => {
+ const cardStyle = PACKAGE_CARD_STYLES.find((style) => style.sessions === pkg.tier)
+
+ return {
+ ...(cardStyle ?? {}),
+ sessions: pkg.sessionsPerMonth,
+ frequency: pkg.typicalFrequency,
+ price: `PKR ${pkg.pricePerMonthPkr.toLocaleString('en-PK')}`,
+ description: cardStyle?.description ?? pkg.description,
+ highlight: cardStyle?.highlight ?? pkg.tier === 12,
+ accentColor: cardStyle?.accentColor ?? '#1040C0',
+ shape: cardStyle?.shape ?? ('square' as const),
+ }
+})
+
const HOW_IT_WORKS = [
{
step: 1,
@@ -353,7 +370,7 @@ export default async function LandingPage() {
- {PACKAGES.map(({ sessions, frequency, price, description, highlight, accentColor, shape }) => (
+ {LANDING_PACKAGES.map(({ sessions, frequency, price, description, highlight, accentColor, shape }) => (
- Book a Free Demo
+ Talk to Admissions
- Not sure yet? Book a no-obligation 30-minute trial session on WhatsApp before committing.
+ Not sure which package fits? Send us your goals and schedule on WhatsApp before committing.
+
+
+ Request a WhatsApp Follow-Up
+
+
+ No account needed for an initial enquiry. Share the details and our team will follow up on WhatsApp.
+
+
+
diff --git a/app/tutor/sessions/page.tsx b/app/tutor/sessions/page.tsx
index f99ff9d..1bbcda7 100644
--- a/app/tutor/sessions/page.tsx
+++ b/app/tutor/sessions/page.tsx
@@ -311,7 +311,10 @@ export default async function TutorSessionsPage({
Join Meet β
)}
-
+
)
diff --git a/components/dashboards/SessionCompleteForm.tsx b/components/dashboards/SessionCompleteForm.tsx
index c33d5aa..d97e13d 100644
--- a/components/dashboards/SessionCompleteForm.tsx
+++ b/components/dashboards/SessionCompleteForm.tsx
@@ -48,9 +48,10 @@ async function tutorUpdateStatusAction(
interface Props {
sessionId: string
+ disabledReason?: string
}
-export function SessionCompleteForm({ sessionId }: Props) {
+export function SessionCompleteForm({ sessionId, disabledReason }: Props) {
const [open, setOpen] = useState(false)
const [state, formAction, isPending] = useActionState(tutorUpdateStatusAction, undefined)
@@ -69,6 +70,12 @@ export function SessionCompleteForm({ sessionId }: Props) {
)
}
+ if (disabledReason) {
+ return (
+ {disabledReason}
+ )
+ }
+
if (!open) {
return (
{
+ test('increments when moving from scheduled to a consuming status', () => {
+ expect(getSessionUsageAdjustment('scheduled', 'done')).toBe(1)
+ expect(getSessionUsageAdjustment('rescheduled', 'no_show_student')).toBe(1)
+ })
+
+ test('does not change counts when moving between consuming statuses', () => {
+ expect(getSessionUsageAdjustment('done', 'no_show_student')).toBe(0)
+ expect(getSessionUsageAdjustment('no_show_student', 'done')).toBe(0)
+ })
+
+ test('decrements when moving from a consuming status to a non-consuming status', () => {
+ expect(getSessionUsageAdjustment('done', 'no_show_tutor')).toBe(-1)
+ expect(getSessionUsageAdjustment('no_show_student', 'rescheduled')).toBe(-1)
+ })
+})
+
+describe('isSessionCompletionAllowed', () => {
+ test('blocks completion/no-show before the scheduled start', () => {
+ const now = new Date('2026-04-08T12:00:00.000Z')
+
+ expect(
+ isSessionCompletionAllowed({
+ scheduledStartUtc: '2026-04-08T12:01:00.000Z',
+ now,
+ }),
+ ).toBe(false)
+ })
+
+ test('allows completion/no-show at or after the scheduled start', () => {
+ const now = new Date('2026-04-08T12:00:00.000Z')
+
+ expect(
+ isSessionCompletionAllowed({
+ scheduledStartUtc: '2026-04-08T12:00:00.000Z',
+ now,
+ }),
+ ).toBe(true)
+ })
+})
diff --git a/lib/services/session-status-transitions.ts b/lib/services/session-status-transitions.ts
new file mode 100644
index 0000000..996e2ac
--- /dev/null
+++ b/lib/services/session-status-transitions.ts
@@ -0,0 +1,31 @@
+import type { Database } from '@/lib/supabase/database.types'
+
+export type SessionStatus = Database['public']['Enums']['session_status_enum']
+
+const SESSION_CONSUMING_STATUSES = ['done', 'no_show_student'] as const satisfies SessionStatus[]
+
+function isConsuming(status: SessionStatus): boolean {
+ return SESSION_CONSUMING_STATUSES.includes(status as (typeof SESSION_CONSUMING_STATUSES)[number])
+}
+
+export function getSessionUsageAdjustment(
+ previousStatus: SessionStatus,
+ nextStatus: SessionStatus,
+): -1 | 0 | 1 {
+ const wasConsuming = isConsuming(previousStatus)
+ const isNowConsuming = isConsuming(nextStatus)
+
+ if (isNowConsuming && !wasConsuming) return 1
+ if (wasConsuming && !isNowConsuming) return -1
+ return 0
+}
+
+export function isSessionCompletionAllowed({
+ scheduledStartUtc,
+ now = new Date(),
+}: {
+ scheduledStartUtc: string
+ now?: Date
+}): boolean {
+ return new Date(scheduledStartUtc).getTime() <= now.getTime()
+}
diff --git a/lib/services/sessions.ts b/lib/services/sessions.ts
index beab937..bcfefe9 100644
--- a/lib/services/sessions.ts
+++ b/lib/services/sessions.ts
@@ -6,6 +6,10 @@
import { createAdminClient } from "@/lib/supabase/admin";
import { createClient } from "@/lib/supabase/server";
import { generateSessions as generateSessionSlots, SchedulePattern } from "@/lib/services/scheduling";
+import {
+ getSessionUsageAdjustment,
+ isSessionCompletionAllowed,
+} from "@/lib/services/session-status-transitions";
import { requireAdmin } from "@/lib/auth/requireAdmin";
import { revalidatePath } from "next/cache";
@@ -231,17 +235,10 @@ export async function deleteSessionsForMatch(
// ββ T8.3: Session Status Update (admin) ββββββββββββββββββββββββββββββββββββββ
-const SESSION_CONSUMING_STATUSES = ["done", "no_show_student"] as const;
-type ConsumingStatus = (typeof SESSION_CONSUMING_STATUSES)[number];
-
-function isConsuming(status: string): status is ConsumingStatus {
- return SESSION_CONSUMING_STATUSES.includes(status as ConsumingStatus);
-}
-
/**
* Admin: update a session's status (and optionally tutor notes).
* Guards against double-incrementing sessions_used when transitioning
- * between two consuming statuses (done β no_show_student).
+ * between consuming/non-consuming statuses.
*/
export async function updateSessionStatus({
sessionId,
@@ -260,7 +257,7 @@ export async function updateSessionStatus({
const adminUserId = await requireAdmin();
const admin = createAdminClient();
- // Fetch current session status to guard against double-incrementing
+ // Fetch current session status to keep package usage counters in sync.
const { data: current, error: fetchErr } = await admin
.from("sessions")
.select("status")
@@ -282,16 +279,21 @@ export async function updateSessionStatus({
if (updateErr)
throw new Error(`Failed to update session: ${updateErr.message}`);
- // Increment sessions_used only when transitioning INTO a consuming status
- // from a non-consuming status β prevents double-incrementing.
- const wasAlreadyConsuming = isConsuming(current.status);
- if (isConsuming(status) && !wasAlreadyConsuming) {
+ const usageAdjustment = getSessionUsageAdjustment(current.status, status);
+ if (usageAdjustment === 1) {
const { error: rpcErr } = await admin.rpc("increment_sessions_used", {
p_request_id: requestId,
});
if (rpcErr)
throw new Error(`Failed to increment sessions_used: ${rpcErr.message}`);
}
+ if (usageAdjustment === -1) {
+ const { error: rpcErr } = await admin.rpc("decrement_sessions_used", {
+ p_request_id: requestId,
+ });
+ if (rpcErr)
+ throw new Error(`Failed to decrement sessions_used: ${rpcErr.message}`);
+ }
// Audit log
await admin.from("audit_logs").insert([
@@ -340,6 +342,17 @@ export async function tutorUpdateSessionStatus({
await requireAuthenticatedUser();
const supabase = await createClient();
+ const { data: session, error: sessionErr } = await supabase
+ .from("sessions")
+ .select("scheduled_start_utc")
+ .eq("id", sessionId)
+ .single();
+
+ if (sessionErr || !session) throw new Error("Session not found.");
+ if (!isSessionCompletionAllowed({ scheduledStartUtc: session.scheduled_start_utc })) {
+ throw new Error("Session has not started yet.");
+ }
+
// Use the security-definer RPC which handles authorization and sessions_used increment
const { error } = await supabase.rpc("tutor_update_session", {
p_session_id: sessionId,
diff --git a/lib/supabase/database.types.ts b/lib/supabase/database.types.ts
index 057e1a8..fd3ba2b 100644
--- a/lib/supabase/database.types.ts
+++ b/lib/supabase/database.types.ts
@@ -618,6 +618,19 @@ export type Database = {
}
Returns: undefined
}
+ decrement_sessions_used: {
+ Args: {
+ p_request_id: string
+ }
+ Returns: undefined
+ }
+ checkout_package: {
+ Args: {
+ p_request_id: string
+ p_tier_sessions: number
+ }
+ Returns: string
+ }
tutor_update_session: {
Args: {
p_session_id: string
diff --git a/package-lock.json b/package-lock.json
index 6b8d533..683ec59 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"luxon": "^3.7.2",
- "next": "16.1.6",
+ "next": "16.2.2",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
@@ -34,11 +34,11 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
- "eslint-config-next": "16.1.6",
- "supabase": "^2.76.15",
+ "eslint-config-next": "16.2.2",
+ "supabase": "^2.87.2",
"tailwindcss": "^4",
"typescript": "^5.9.3",
- "vitest": "^4.0.18"
+ "vitest": "^4.1.3"
}
},
"node_modules/@alloc/quick-lru": {
@@ -296,21 +296,21 @@
}
},
"node_modules/@emnapi/core": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
- "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/wasi-threads": "1.1.0",
+ "@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
- "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -318,456 +318,14 @@
}
},
"node_modules/@emnapi/wasi-threads": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
- "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
- "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
- "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
- "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
- "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
- "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
- "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
- "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
- "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
- "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
- "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
- "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
- "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
- "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
- "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
- "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
- "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
- "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
- "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
- "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
- "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
- "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
- "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
- "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
- "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
- "cpu": [
- "x64"
- ],
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -1559,15 +1117,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
- "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz",
+ "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz",
- "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.2.tgz",
+ "integrity": "sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1575,9 +1133,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
- "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz",
+ "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==",
"cpu": [
"arm64"
],
@@ -1591,9 +1149,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
- "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz",
+ "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==",
"cpu": [
"x64"
],
@@ -1607,9 +1165,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
- "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz",
+ "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==",
"cpu": [
"arm64"
],
@@ -1623,9 +1181,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
- "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz",
+ "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==",
"cpu": [
"arm64"
],
@@ -1639,9 +1197,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
- "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz",
+ "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==",
"cpu": [
"x64"
],
@@ -1655,9 +1213,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
- "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz",
+ "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==",
"cpu": [
"x64"
],
@@ -1671,9 +1229,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
- "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz",
+ "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==",
"cpu": [
"arm64"
],
@@ -1687,9 +1245,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
- "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz",
+ "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==",
"cpu": [
"x64"
],
@@ -1750,6 +1308,16 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@oxc-project/types": {
+ "version": "0.123.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz",
+ "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -3265,24 +2833,10 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
- "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
- "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==",
"cpu": [
"arm64"
],
@@ -3291,12 +2845,15 @@
"optional": true,
"os": [
"android"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
- "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==",
"cpu": [
"arm64"
],
@@ -3305,12 +2862,15 @@
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
- "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==",
"cpu": [
"x64"
],
@@ -3319,26 +2879,15 @@
"optional": true,
"os": [
"darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
- "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
- "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==",
"cpu": [
"x64"
],
@@ -3347,26 +2896,15 @@
"optional": true,
"os": [
"freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
- "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
- "cpu": [
- "arm"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
- "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz",
+ "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==",
"cpu": [
"arm"
],
@@ -3375,26 +2913,15 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
- "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
- "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==",
"cpu": [
"arm64"
],
@@ -3403,54 +2930,32 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
- "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
- "cpu": [
- "loong64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
- "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz",
+ "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==",
"cpu": [
- "loong64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
- "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
- "cpu": [
- "ppc64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
- "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==",
"cpu": [
"ppc64"
],
@@ -3459,40 +2964,15 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
- "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
- "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
- "cpu": [
- "riscv64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
- "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==",
"cpu": [
"s390x"
],
@@ -3501,12 +2981,15 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==",
"cpu": [
"x64"
],
@@ -3515,12 +2998,15 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
- "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz",
+ "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==",
"cpu": [
"x64"
],
@@ -3529,82 +3015,98 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
- "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "openbsd"
- ]
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
- "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz",
+ "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==",
"cpu": [
- "arm64"
+ "wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "openharmony"
- ]
+ "dependencies": {
+ "@emnapi/core": "1.9.1",
+ "@emnapi/runtime": "1.9.1",
+ "@napi-rs/wasm-runtime": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
},
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
- "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
},
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
- "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
+ "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz",
+ "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
- "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz",
+ "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==",
"cpu": [
"x64"
],
@@ -3613,7 +3115,17 @@
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
+ "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
@@ -4300,37 +3812,24 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
- "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
+ "balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
- "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^5.0.2"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4677,31 +4176,31 @@
]
},
"node_modules/@vitest/expect": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
- "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz",
+ "integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.0.18",
- "@vitest/utils": "4.0.18",
- "chai": "^6.2.1",
- "tinyrainbow": "^3.0.3"
+ "@vitest/spy": "4.1.3",
+ "@vitest/utils": "4.1.3",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
- "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz",
+ "integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.0.18",
+ "@vitest/spy": "4.1.3",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -4710,7 +4209,7 @@
},
"peerDependencies": {
"msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0-0"
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"msw": {
@@ -4722,26 +4221,26 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
- "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz",
+ "integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
- "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz",
+ "integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.0.18",
+ "@vitest/utils": "4.1.3",
"pathe": "^2.0.3"
},
"funding": {
@@ -4749,13 +4248,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
- "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz",
+ "integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.18",
+ "@vitest/pretty-format": "4.1.3",
+ "@vitest/utils": "4.1.3",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -4764,9 +4264,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
- "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz",
+ "integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4774,14 +4274,15 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
- "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz",
+ "integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.18",
- "tinyrainbow": "^3.0.3"
+ "@vitest/pretty-format": "4.1.3",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4812,13 +4313,13 @@
}
},
"node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
+ "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 14"
+ "node": ">= 20"
}
},
"node_modules/ajv": {
@@ -5746,9 +5247,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"dev": true,
"license": "MIT"
},
@@ -5803,55 +5304,13 @@
"dependencies": {
"is-callable": "^1.2.7",
"is-date-object": "^1.0.5",
- "is-symbol": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/esbuild": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
- "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
+ "is-symbol": "^1.0.4"
},
"engines": {
- "node": ">=18"
+ "node": ">= 0.4"
},
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/escalade": {
@@ -5939,13 +5398,13 @@
}
},
"node_modules/eslint-config-next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz",
- "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.2.tgz",
+ "integrity": "sha512-6VlvEhwoug2JpVgjZDhyXrJXUEuPY++TddzIpTaIRvlvlXXFgvQUtm3+Zr84IjFm0lXtJt73w19JA08tOaZVwg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@next/eslint-plugin-next": "16.1.6",
+ "@next/eslint-plugin-next": "16.2.2",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -6472,9 +5931,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -6837,17 +6296,17 @@
}
},
"node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz",
+ "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
+ "agent-base": "9.0.0",
+ "debug": "^4.3.4"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 20"
}
},
"node_modules/iceberg-js": {
@@ -7865,9 +7324,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
- "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -7959,14 +7418,14 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.1.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
- "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
+ "version": "16.2.2",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz",
+ "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.6",
+ "@next/env": "16.2.2",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -7978,15 +7437,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.6",
- "@next/swc-darwin-x64": "16.1.6",
- "@next/swc-linux-arm64-gnu": "16.1.6",
- "@next/swc-linux-arm64-musl": "16.1.6",
- "@next/swc-linux-x64-gnu": "16.1.6",
- "@next/swc-linux-x64-musl": "16.1.6",
- "@next/swc-win32-arm64-msvc": "16.1.6",
- "@next/swc-win32-x64-msvc": "16.1.6",
- "sharp": "^0.34.4"
+ "@next/swc-darwin-arm64": "16.2.2",
+ "@next/swc-darwin-x64": "16.2.2",
+ "@next/swc-linux-arm64-gnu": "16.2.2",
+ "@next/swc-linux-arm64-musl": "16.2.2",
+ "@next/swc-linux-x64-gnu": "16.2.2",
+ "@next/swc-linux-x64-musl": "16.2.2",
+ "@next/swc-win32-arm64-msvc": "16.2.2",
+ "@next/swc-win32-x64-msvc": "16.2.2",
+ "sharp": "^0.34.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -8381,9 +7840,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8436,9 +7895,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"dev": true,
"funding": [
{
@@ -8826,49 +8285,38 @@
"node": ">=0.10.0"
}
},
- "node_modules/rollup": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
- "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz",
+ "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
+ "@oxc-project/types": "=0.123.0",
+ "@rolldown/pluginutils": "1.0.0-rc.13"
},
"bin": {
- "rollup": "dist/bin/rollup"
+ "rolldown": "bin/cli.mjs"
},
"engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.59.0",
- "@rollup/rollup-android-arm64": "4.59.0",
- "@rollup/rollup-darwin-arm64": "4.59.0",
- "@rollup/rollup-darwin-x64": "4.59.0",
- "@rollup/rollup-freebsd-arm64": "4.59.0",
- "@rollup/rollup-freebsd-x64": "4.59.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
- "@rollup/rollup-linux-arm64-gnu": "4.59.0",
- "@rollup/rollup-linux-arm64-musl": "4.59.0",
- "@rollup/rollup-linux-loong64-gnu": "4.59.0",
- "@rollup/rollup-linux-loong64-musl": "4.59.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
- "@rollup/rollup-linux-ppc64-musl": "4.59.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
- "@rollup/rollup-linux-riscv64-musl": "4.59.0",
- "@rollup/rollup-linux-s390x-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-musl": "4.59.0",
- "@rollup/rollup-openbsd-x64": "4.59.0",
- "@rollup/rollup-openharmony-arm64": "4.59.0",
- "@rollup/rollup-win32-arm64-msvc": "4.59.0",
- "@rollup/rollup-win32-ia32-msvc": "4.59.0",
- "@rollup/rollup-win32-x64-gnu": "4.59.0",
- "@rollup/rollup-win32-x64-msvc": "4.59.0",
- "fsevents": "~2.3.2"
+ "@rolldown/binding-android-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.13",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13"
}
},
"node_modules/run-parallel": {
@@ -9232,9 +8680,9 @@
"license": "MIT"
},
"node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
"dev": true,
"license": "MIT"
},
@@ -9412,17 +8860,17 @@
}
},
"node_modules/supabase": {
- "version": "2.76.15",
- "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.76.15.tgz",
- "integrity": "sha512-m69o1XPAzZaIWfQiEeT+KY/Ci3OSA663RyoH9xECbXSxhr7dsipLCpCqT1E4MCob0mMhHh/7A+Eltx4y1qSwiQ==",
+ "version": "2.87.2",
+ "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.87.2.tgz",
+ "integrity": "sha512-bd2RpMFQ+U9xy/D6V2q/U98Ta9Is0Q/Id6UpsZAWGeuadZNLxW80GhZCu07w4uWZsLiRa4Eq1LS9cfzLEo3Z3g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bin-links": "^6.0.0",
- "https-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^9.0.0",
"node-fetch": "^3.3.2",
- "tar": "7.5.9"
+ "tar": "7.5.13"
},
"bin": {
"supabase": "bin/supabase"
@@ -9489,9 +8937,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
- "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
+ "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -9568,9 +9016,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -9582,9 +9030,9 @@
}
},
"node_modules/tinyrainbow": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
- "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9933,17 +9381,17 @@
}
},
"node_modules/vite": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
- "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
+ "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "esbuild": "^0.27.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.13",
"tinyglobby": "^0.2.15"
},
"bin": {
@@ -9960,9 +9408,10 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
- "lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
@@ -9975,13 +9424,16 @@
"@types/node": {
"optional": true
},
- "jiti": {
+ "@vitejs/devtools": {
"optional": true
},
- "less": {
+ "esbuild": {
"optional": true
},
- "lightningcss": {
+ "jiti": {
+ "optional": true
+ },
+ "less": {
"optional": true
},
"sass": {
@@ -10007,24 +9459,6 @@
}
}
},
- "node_modules/vite/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/vite/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -10040,13 +9474,273 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/vite/node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/vite/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -10055,31 +9749,31 @@
}
},
"node_modules/vitest": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
- "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz",
+ "integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.0.18",
- "@vitest/mocker": "4.0.18",
- "@vitest/pretty-format": "4.0.18",
- "@vitest/runner": "4.0.18",
- "@vitest/snapshot": "4.0.18",
- "@vitest/spy": "4.0.18",
- "@vitest/utils": "4.0.18",
- "es-module-lexer": "^1.7.0",
- "expect-type": "^1.2.2",
+ "@vitest/expect": "4.1.3",
+ "@vitest/mocker": "4.1.3",
+ "@vitest/pretty-format": "4.1.3",
+ "@vitest/runner": "4.1.3",
+ "@vitest/snapshot": "4.1.3",
+ "@vitest/spy": "4.1.3",
+ "@vitest/utils": "4.1.3",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
- "std-env": "^3.10.0",
+ "std-env": "^4.0.0-rc.1",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.0.3",
- "vite": "^6.0.0 || ^7.0.0",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -10095,12 +9789,15 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.0.18",
- "@vitest/browser-preview": "4.0.18",
- "@vitest/browser-webdriverio": "4.0.18",
- "@vitest/ui": "4.0.18",
+ "@vitest/browser-playwright": "4.1.3",
+ "@vitest/browser-preview": "4.1.3",
+ "@vitest/browser-webdriverio": "4.1.3",
+ "@vitest/coverage-istanbul": "4.1.3",
+ "@vitest/coverage-v8": "4.1.3",
+ "@vitest/ui": "4.1.3",
"happy-dom": "*",
- "jsdom": "*"
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
@@ -10121,6 +9818,12 @@
"@vitest/browser-webdriverio": {
"optional": true
},
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
"@vitest/ui": {
"optional": true
},
@@ -10129,13 +9832,16 @@
},
"jsdom": {
"optional": true
+ },
+ "vite": {
+ "optional": false
}
}
},
"node_modules/vitest/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index cbf14a4..cccc46e 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,9 @@
"start": "next start",
"lint": "eslint .",
"test:e2e": "playwright test",
+ "test:e2e:local": "node scripts/with-local-supabase-env.mjs npm run test:e2e",
+ "build:local": "node scripts/with-local-supabase-env.mjs npm run build",
+ "dev:local": "node scripts/with-local-supabase-env.mjs npm run dev",
"test:e2e:ui": "playwright test --ui",
"typecheck": "tsc --noEmit",
"test": "vitest run",
@@ -21,7 +24,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"luxon": "^3.7.2",
- "next": "16.1.6",
+ "next": "16.2.2",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
@@ -40,10 +43,10 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
- "eslint-config-next": "16.1.6",
- "supabase": "^2.76.15",
+ "eslint-config-next": "16.2.2",
+ "supabase": "^2.87.2",
"tailwindcss": "^4",
"typescript": "^5.9.3",
- "vitest": "^4.0.18"
+ "vitest": "^4.1.3"
}
}
diff --git a/middleware.ts b/proxy.ts
similarity index 98%
rename from middleware.ts
rename to proxy.ts
index f96984e..d0334c3 100644
--- a/middleware.ts
+++ b/proxy.ts
@@ -5,7 +5,7 @@
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
-export async function middleware(request: NextRequest) {
+export async function proxy(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
diff --git a/scripts/with-local-supabase-env.mjs b/scripts/with-local-supabase-env.mjs
new file mode 100644
index 0000000..68631a7
--- /dev/null
+++ b/scripts/with-local-supabase-env.mjs
@@ -0,0 +1,61 @@
+import { spawnSync } from 'node:child_process'
+
+const command = process.argv[2]
+const commandArgs = process.argv.slice(3)
+
+if (!command) {
+ console.error('Usage: node scripts/with-local-supabase-env.mjs [...args]')
+ process.exit(1)
+}
+
+const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx'
+const statusResult = spawnSync(npxCommand, ['supabase', 'status', '-o', 'env'], {
+ encoding: 'utf8',
+ stdio: ['ignore', 'pipe', 'inherit'],
+ shell: process.platform === 'win32',
+})
+
+if (statusResult.status !== 0) {
+ process.exit(statusResult.status ?? 1)
+}
+
+const statusOutput = statusResult.stdout
+
+const localEnv = Object.fromEntries(
+ statusOutput
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter((line) => line && line.includes('='))
+ .map((line) => {
+ const [key, ...valueParts] = line.split('=')
+ const value = valueParts.join('=').replace(/^"|"$/g, '')
+ return [key, value]
+ }),
+)
+
+const env = {
+ ...process.env,
+ NEXT_PUBLIC_SUPABASE_URL: localEnv.API_URL,
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: localEnv.ANON_KEY,
+ SUPABASE_SERVICE_ROLE_KEY: localEnv.SERVICE_ROLE_KEY,
+ CRON_SECRET: process.env.CRON_SECRET || 'local-cron-secret',
+ NEXT_PUBLIC_WHATSAPP_NUMBER: process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '+923001234567',
+ NEXT_PUBLIC_BANK_NAME: process.env.NEXT_PUBLIC_BANK_NAME || 'Local Test Bank',
+ NEXT_PUBLIC_BANK_ACCOUNT_TITLE:
+ process.env.NEXT_PUBLIC_BANK_ACCOUNT_TITLE || 'CorvEd Local Test',
+ NEXT_PUBLIC_BANK_ACCOUNT_NUMBER:
+ process.env.NEXT_PUBLIC_BANK_ACCOUNT_NUMBER || 'LOCAL-TEST-IBAN',
+}
+
+if (!env.NEXT_PUBLIC_SUPABASE_URL || !env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
+ console.error('Local Supabase env was not available. Is Docker Supabase running?')
+ process.exit(1)
+}
+
+const result = spawnSync(command, commandArgs, {
+ env,
+ shell: process.platform === 'win32',
+ stdio: 'inherit',
+})
+
+process.exit(result.status ?? 1)
diff --git a/supabase/__tests__/lifecycle-hardening.test.ts b/supabase/__tests__/lifecycle-hardening.test.ts
new file mode 100644
index 0000000..1346161
--- /dev/null
+++ b/supabase/__tests__/lifecycle-hardening.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, test } from 'vitest'
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+
+const migrationSql = readFileSync(
+ join(
+ process.cwd(),
+ 'supabase/migrations/20260408000001_harden_lifecycle_rls_and_checkout.sql',
+ ),
+ 'utf8',
+)
+
+describe('lifecycle RLS hardening migration', () => {
+ test('constrains client-controlled initial request, package, and payment states', () => {
+ expect(migrationSql).toContain('requests_insert_self')
+ expect(migrationSql).toContain("requests.status = 'new'")
+ expect(migrationSql).toContain("packages.status = 'pending'")
+ expect(migrationSql).toContain('packages.sessions_total = packages.tier_sessions')
+ expect(migrationSql).toContain("payments.status = 'pending'")
+ expect(migrationSql).toContain('payments.verified_by_user_id is null')
+ })
+
+ test('adds a transactional checkout RPC and duplicate package guard', () => {
+ expect(migrationSql).toContain('create or replace function public.checkout_package')
+ expect(migrationSql).toContain('packages_one_open_package_per_request')
+ expect(migrationSql).toContain("status in ('pending', 'active')")
+ })
+
+ test('blocks tutor completion of future sessions in the RPC', () => {
+ expect(migrationSql).toContain('v_scheduled_start_utc')
+ expect(migrationSql).toContain("raise exception 'session has not started yet'")
+ })
+})
diff --git a/supabase/__tests__/lifecycle-rls.integration.test.ts b/supabase/__tests__/lifecycle-rls.integration.test.ts
new file mode 100644
index 0000000..34233b1
--- /dev/null
+++ b/supabase/__tests__/lifecycle-rls.integration.test.ts
@@ -0,0 +1,131 @@
+import { beforeAll, describe, expect, test } from 'vitest'
+import { createClient } from '@supabase/supabase-js'
+import type { Database } from '@/lib/supabase/database.types'
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
+const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
+const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
+
+const hasLocalSupabaseEnv = Boolean(supabaseUrl && anonKey && serviceRoleKey)
+const describeLocal = hasLocalSupabaseEnv ? describe : describe.skip
+
+describeLocal('lifecycle RLS and checkout RPC', () => {
+ const password = 'LocalRlsTest!12345'
+ const unique = Date.now()
+ const email = `rls-${unique}@example.com`
+
+ let admin: ReturnType>
+ let student: ReturnType>
+ let studentUserId: string
+ let subjectId: number
+
+ beforeAll(async () => {
+ admin = createClient(supabaseUrl!, serviceRoleKey!, {
+ auth: { autoRefreshToken: false, persistSession: false },
+ })
+ student = createClient(supabaseUrl!, anonKey!, {
+ auth: { autoRefreshToken: false, persistSession: false },
+ })
+
+ const { data: created, error: createError } = await admin.auth.admin.createUser({
+ email,
+ password,
+ email_confirm: true,
+ })
+ if (createError || !created.user) throw createError ?? new Error('Failed to create user')
+ studentUserId = created.user.id
+
+ await admin.from('user_profiles').upsert({
+ user_id: studentUserId,
+ display_name: 'RLS Student',
+ whatsapp_number: '+923001234567',
+ timezone: 'Asia/Karachi',
+ primary_role: 'student',
+ })
+ await admin.from('user_roles').upsert({ user_id: studentUserId, role: 'student' })
+
+ const { data: subject, error: subjectError } = await admin
+ .from('subjects')
+ .select('id')
+ .eq('active', true)
+ .limit(1)
+ .single()
+ if (subjectError || !subject) throw subjectError ?? new Error('No active subject found')
+ subjectId = subject.id
+
+ const { error: signInError } = await student.auth.signInWithPassword({ email, password })
+ if (signInError) throw signInError
+ })
+
+ test('blocks advanced request states and creates checkout atomically', async () => {
+ const baseRequest = {
+ created_by_user_id: studentUserId,
+ requester_role: 'student' as const,
+ level: 'o_levels' as const,
+ subject_id: subjectId,
+ exam_board: 'unspecified' as const,
+ timezone: 'Asia/Karachi',
+ availability_windows: [],
+ }
+
+ const { error: advancedRequestError } = await student
+ .from('requests')
+ .insert({ ...baseRequest, status: 'active' })
+ expect(advancedRequestError).toBeTruthy()
+
+ const { data: request, error: requestError } = await student
+ .from('requests')
+ .insert(baseRequest)
+ .select('id')
+ .single()
+ expect(requestError).toBeNull()
+ expect(request).toBeTruthy()
+
+ const { error: advancedPackageError } = await student.from('packages').insert({
+ request_id: request!.id,
+ tier_sessions: 8,
+ start_date: '2026-04-08',
+ end_date: '2026-05-08',
+ sessions_total: 8,
+ sessions_used: 0,
+ status: 'active',
+ })
+ expect(advancedPackageError).toBeTruthy()
+
+ const { data: packageId, error: checkoutError } = await student.rpc('checkout_package', {
+ p_request_id: request!.id,
+ p_tier_sessions: 8,
+ })
+ expect(checkoutError).toBeNull()
+ expect(packageId).toEqual(expect.any(String))
+
+ const [{ data: pkg }, { data: payment }, { data: updatedRequest }] = await Promise.all([
+ admin.from('packages').select('status, sessions_total, sessions_used').eq('id', packageId!).single(),
+ admin.from('payments').select('status, amount_pkr, verified_by_user_id').eq('package_id', packageId!).single(),
+ admin.from('requests').select('status').eq('id', request!.id).single(),
+ ])
+
+ expect(pkg).toMatchObject({ status: 'pending', sessions_total: 8, sessions_used: 0 })
+ expect(payment).toMatchObject({ status: 'pending', amount_pkr: 8000, verified_by_user_id: null })
+ expect(updatedRequest).toMatchObject({ status: 'payment_pending' })
+
+ const { data: repeatedPackageId, error: repeatedCheckoutError } = await student.rpc(
+ 'checkout_package',
+ {
+ p_request_id: request!.id,
+ p_tier_sessions: 8,
+ },
+ )
+ expect(repeatedCheckoutError).toBeNull()
+ expect(repeatedPackageId).toBe(packageId)
+
+ const { error: paidPaymentError } = await student.from('payments').insert({
+ package_id: packageId!,
+ payer_user_id: studentUserId,
+ amount_pkr: 8000,
+ method: 'bank_transfer',
+ status: 'paid',
+ })
+ expect(paidPaymentError).toBeTruthy()
+ })
+})
diff --git a/supabase/migrations/20260408000001_harden_lifecycle_rls_and_checkout.sql b/supabase/migrations/20260408000001_harden_lifecycle_rls_and_checkout.sql
new file mode 100644
index 0000000..c0662ef
--- /dev/null
+++ b/supabase/migrations/20260408000001_harden_lifecycle_rls_and_checkout.sql
@@ -0,0 +1,298 @@
+-- Harden client-controlled lifecycle state and move package checkout into one RPC.
+
+-- Guard against duplicate pending/active packages for the same request.
+create unique index if not exists packages_one_open_package_per_request
+ on public.packages (request_id)
+ where status in ('pending', 'active');
+
+-- Requests: users can create only their own initial request state.
+drop policy if exists "requests_insert_self" on public.requests;
+create policy "requests_insert_self"
+ on public.requests for insert to authenticated
+ with check (
+ requests.created_by_user_id = auth.uid()
+ and requests.status = 'new'
+ and requests.requester_role in ('student', 'parent')
+ );
+
+-- Packages: client inserts must remain initial, owned, and internally consistent.
+drop policy if exists "packages_insert_creator" on public.packages;
+create policy "packages_insert_creator"
+ on public.packages for insert to authenticated
+ with check (
+ packages.status = 'pending'
+ and packages.tier_sessions in (8, 12, 20)
+ and packages.sessions_total = packages.tier_sessions
+ and packages.sessions_used = 0
+ and packages.end_date >= packages.start_date
+ and exists (
+ select 1
+ from public.requests r
+ where r.id = packages.request_id
+ and r.created_by_user_id = auth.uid()
+ and r.status in ('new', 'payment_pending')
+ )
+ );
+
+-- Payments: client inserts/updates must stay pending, owned, and unverified.
+drop policy if exists "payments_insert_payer" on public.payments;
+create policy "payments_insert_payer"
+ on public.payments for insert to authenticated
+ with check (
+ payments.payer_user_id = auth.uid()
+ and payments.status = 'pending'
+ and payments.method = 'bank_transfer'
+ and payments.rejection_note is null
+ and payments.verified_by_user_id is null
+ and payments.verified_at is null
+ and exists (
+ select 1
+ from public.packages p
+ join public.requests r on r.id = p.request_id
+ where p.id = payments.package_id
+ and r.created_by_user_id = auth.uid()
+ and payments.amount_pkr = case p.tier_sessions
+ when 8 then 8000
+ when 12 then 11000
+ when 20 then 16000
+ end
+ )
+ );
+
+drop policy if exists "payments_update_payer_limited" on public.payments;
+create policy "payments_update_payer_limited"
+ on public.payments for update to authenticated
+ using (
+ payments.payer_user_id = auth.uid()
+ and payments.status = 'pending'
+ )
+ with check (
+ payments.payer_user_id = auth.uid()
+ and payments.status = 'pending'
+ and payments.method = 'bank_transfer'
+ and payments.rejection_note is null
+ and payments.verified_by_user_id is null
+ and payments.verified_at is null
+ and exists (
+ select 1
+ from public.packages p
+ join public.requests r on r.id = p.request_id
+ where p.id = payments.package_id
+ and r.created_by_user_id = auth.uid()
+ and payments.amount_pkr = case p.tier_sessions
+ when 8 then 8000
+ when 12 then 11000
+ when 20 then 16000
+ end
+ )
+ );
+
+create or replace function public.checkout_package(
+ p_request_id uuid,
+ p_tier_sessions int
+)
+returns uuid
+language plpgsql
+security definer
+set search_path = public, pg_temp
+as $$
+declare
+ v_user_id uuid := auth.uid();
+ v_package_id uuid;
+ v_existing_tier int;
+ v_amount int;
+ v_start_date date := current_date;
+ v_end_date date := (current_date + interval '1 month')::date;
+begin
+ if v_user_id is null then
+ raise exception 'not authenticated';
+ end if;
+
+ if p_tier_sessions not in (8, 12, 20) then
+ raise exception 'invalid package tier';
+ end if;
+
+ select case p_tier_sessions
+ when 8 then 8000
+ when 12 then 11000
+ when 20 then 16000
+ end into v_amount;
+
+ select p.id, p.tier_sessions
+ into v_package_id, v_existing_tier
+ from public.packages p
+ join public.requests r on r.id = p.request_id
+ where p.request_id = p_request_id
+ and r.created_by_user_id = v_user_id
+ and p.status in ('pending', 'active')
+ order by p.created_at desc
+ limit 1;
+
+ if v_package_id is not null then
+ select case v_existing_tier
+ when 8 then 8000
+ when 12 then 11000
+ when 20 then 16000
+ end into v_amount;
+
+ insert into public.payments (
+ package_id,
+ payer_user_id,
+ amount_pkr,
+ method,
+ status
+ )
+ select
+ v_package_id,
+ v_user_id,
+ v_amount,
+ 'bank_transfer',
+ 'pending'
+ where not exists (
+ select 1 from public.payments pay where pay.package_id = v_package_id
+ );
+
+ return v_package_id;
+ end if;
+
+ perform 1
+ from public.requests r
+ where r.id = p_request_id
+ and r.created_by_user_id = v_user_id
+ and r.status in ('new', 'payment_pending')
+ for update;
+
+ if not found then
+ raise exception 'request not available for checkout';
+ end if;
+
+ insert into public.packages (
+ request_id,
+ tier_sessions,
+ start_date,
+ end_date,
+ sessions_total,
+ sessions_used,
+ status
+ )
+ values (
+ p_request_id,
+ p_tier_sessions,
+ v_start_date,
+ v_end_date,
+ p_tier_sessions,
+ 0,
+ 'pending'
+ )
+ returning id into v_package_id;
+
+ update public.requests
+ set status = 'payment_pending',
+ updated_at = now()
+ where id = p_request_id
+ and created_by_user_id = v_user_id
+ and status = 'new';
+
+ insert into public.payments (
+ package_id,
+ payer_user_id,
+ amount_pkr,
+ method,
+ status
+ )
+ values (
+ v_package_id,
+ v_user_id,
+ v_amount,
+ 'bank_transfer',
+ 'pending'
+ );
+
+ return v_package_id;
+end;
+$$;
+
+revoke execute on function public.checkout_package(uuid, int) from public, anon;
+grant execute on function public.checkout_package(uuid, int) to authenticated;
+
+-- Replace tutor_update_session with a future-session guard plus existing usage delta logic.
+create or replace function public.tutor_update_session(
+ p_session_id uuid,
+ p_status public.session_status_enum,
+ p_notes text
+)
+returns void
+language plpgsql
+security definer
+set search_path = public, pg_temp
+as $$
+declare
+ v_match_id uuid;
+ v_tutor_id uuid;
+ v_request_id uuid;
+ v_prev_status public.session_status_enum;
+ v_scheduled_start_utc timestamptz;
+ v_old_consuming boolean;
+ v_new_consuming boolean;
+begin
+ select status, match_id, scheduled_start_utc
+ into v_prev_status, v_match_id, v_scheduled_start_utc
+ from public.sessions
+ where id = p_session_id;
+
+ if v_match_id is null then
+ raise exception 'session not found';
+ end if;
+
+ select tutor_user_id, request_id
+ into v_tutor_id, v_request_id
+ from public.matches
+ where id = v_match_id;
+
+ if v_tutor_id <> auth.uid() and not public.is_admin(auth.uid()) then
+ raise exception 'not authorized';
+ end if;
+
+ if p_status not in ('done', 'no_show_student', 'no_show_tutor')
+ and not public.is_admin(auth.uid()) then
+ raise exception 'invalid status for tutor update';
+ end if;
+
+ if p_status in ('done', 'no_show_student', 'no_show_tutor')
+ and v_scheduled_start_utc > now()
+ and not public.is_admin(auth.uid()) then
+ raise exception 'session has not started yet';
+ end if;
+
+ update public.sessions
+ set status = p_status,
+ tutor_notes = p_notes,
+ updated_by_user_id = auth.uid(),
+ updated_at = now()
+ where id = p_session_id;
+
+ v_old_consuming := v_prev_status in ('done', 'no_show_student');
+ v_new_consuming := p_status in ('done', 'no_show_student');
+
+ if v_new_consuming and not v_old_consuming then
+ perform public.increment_sessions_used(v_request_id);
+ end if;
+
+ if v_old_consuming and not v_new_consuming then
+ perform public.decrement_sessions_used(v_request_id);
+ end if;
+
+ insert into public.audit_logs(actor_user_id, action, entity_type, entity_id, details)
+ values (
+ auth.uid(),
+ 'session_status_updated',
+ 'session',
+ p_session_id,
+ jsonb_build_object(
+ 'previous_status', v_prev_status,
+ 'status', p_status,
+ 'tutor_notes', p_notes
+ )
+ );
+end;
+$$;
diff --git a/vitest.config.ts b/vitest.config.ts
index 69e158a..fffcb74 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -6,7 +6,7 @@ export default defineConfig({
globals: true,
environment: 'node',
include: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
- exclude: ['node_modules', 'e2e', '.next'],
+ exclude: ['node_modules', 'e2e', '.next', '.opencode', '.tmp', 'playwright-report', 'test-results'],
},
resolve: {
alias: {
From bc600d3fc780104608d376b029e3dc599ba003d6 Mon Sep 17 00:00:00 2001
From: Taleef7
Date: Tue, 7 Apr 2026 21:19:56 -0400
Subject: [PATCH 2/3] chore: sync package lock optional deps
---
package-lock.json | 64 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index 683ec59..d3833df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3472,6 +3472,70 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz",
From 415e0dd8ac9bdcb54f8b1676913dcac42d23eca5 Mon Sep 17 00:00:00 2001
From: Taleef7
Date: Tue, 7 Apr 2026 21:23:11 -0400
Subject: [PATCH 3/3] chore: complete optional dependency lockfile entries
---
package-lock.json | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index d3833df..327611b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3056,6 +3056,28 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",