diff --git a/apps/web/__tests__/api/ai-caption.test.ts b/apps/web/__tests__/api/ai-caption.test.ts index 2c9ca79e..ce19c6e9 100644 --- a/apps/web/__tests__/api/ai-caption.test.ts +++ b/apps/web/__tests__/api/ai-caption.test.ts @@ -4,6 +4,14 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(), })); +const mockServiceClient = { + rpc: vi.fn(), +}; + +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: vi.fn(() => mockServiceClient), +})); + const mockCreate = vi.fn(); vi.mock("@anthropic-ai/sdk", () => { @@ -48,6 +56,10 @@ beforeEach(() => { vi.clearAllMocks(); process.env.ANTHROPIC_API_KEY = "test-key"; process.env.NEXT_PUBLIC_SUPABASE_URL = "https://test.supabase.co"; + mockServiceClient.rpc.mockResolvedValue({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); }); describe("POST /api/ai/generate-caption", () => { @@ -62,7 +74,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: { costUSD: 2.5, totalTokens: 10000 }, }) ); @@ -81,7 +93,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: { costUSD: 1 }, }) ); @@ -97,7 +109,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: {}, }) ); @@ -135,7 +147,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: {}, }) ); @@ -151,7 +163,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: {}, }) ); @@ -172,7 +184,7 @@ describe("POST /api/ai/generate-caption", () => { const res = await POST( makeRequest({ - images: ["https://test.supabase.co/storage/v1/object/public/screenshots/1.png"], + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], usage: {}, }) ); @@ -182,4 +194,41 @@ describe("POST /api/ai/generate-caption", () => { expect(json.title.length).toBe(100); expect(json.description.length).toBe(5000); }); + + it("rejects first-party storage URLs outside post-images", async () => { + mockSupabaseUser({ id: "user-1" }); + + const res = await POST( + makeRequest({ + images: ["https://test.supabase.co/storage/v1/object/public/avatars/user-1/avatar.png"], + usage: {}, + }) + ); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe("Images must use Straude post image uploads"); + expect(mockCreate).not.toHaveBeenCalled(); + }); + + it("applies durable AI quota before calling Anthropic", async () => { + mockSupabaseUser({ id: "user-1" }); + mockServiceClient.rpc.mockResolvedValueOnce({ + data: [{ allowed: false, retry_after_seconds: 42 }], + error: null, + }); + + const res = await POST( + makeRequest({ + images: ["https://test.supabase.co/storage/v1/object/public/post-images/user-1/1.png"], + usage: {}, + }) + ); + const json = await res.json(); + + expect(res.status).toBe(429); + expect(res.headers.get("Retry-After")).toBe("42"); + expect(json.error).toContain("Too many requests"); + expect(mockCreate).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/__tests__/api/auth-cli.test.ts b/apps/web/__tests__/api/auth-cli.test.ts index 422e05f3..17dc7ba0 100644 --- a/apps/web/__tests__/api/auth-cli.test.ts +++ b/apps/web/__tests__/api/auth-cli.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const mockServiceClient: Record = { from: vi.fn(), + rpc: vi.fn(), }; vi.mock("@/lib/supabase/service", () => ({ @@ -12,9 +13,13 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(), })); -vi.mock("@/lib/api/cli-auth", () => ({ - createCliToken: vi.fn(), -})); +vi.mock("@/lib/api/cli-auth", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createCliToken: vi.fn(), + }; +}); import { POST as initPOST } from "@/app/api/auth/cli/init/route"; import { POST as pollPOST } from "@/app/api/auth/cli/poll/route"; @@ -30,6 +35,7 @@ function mockChain(overrides = {}) { update: vi.fn().mockReturnThis(), eq: vi.fn().mockReturnThis(), gt: vi.fn().mockReturnThis(), + is: vi.fn().mockReturnThis(), neq: vi.fn().mockReturnThis(), single: vi.fn().mockResolvedValue({ data: null, error: null }), ...overrides, @@ -50,6 +56,10 @@ beforeEach(() => { vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://straude.com"); vi.stubEnv("NEXT_PUBLIC_SUPABASE_URL", "https://test.supabase.co"); vi.stubEnv("SUPABASE_SECRET_KEY", "test-secret"); + mockServiceClient.rpc.mockResolvedValue({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); }); function mockAuthenticatedUser(user: { id: string } | null = { id: "user-abc" }) { @@ -76,9 +86,20 @@ describe("POST /api/auth/cli/init", () => { expect(res.status).toBe(200); expect(json.code).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/); - expect(json.verify_url).toBe( - `https://straude.com/cli/verify?code=${json.code}` - ); + expect(json.poll_secret).toMatch(/^[A-Za-z0-9_-]+$/); + + const url = new URL(json.verify_url); + expect(`${url.origin}${url.pathname}`).toBe("https://straude.com/cli/verify"); + expect(url.searchParams.get("code")).toBe(json.code); + expect(url.searchParams.get("verify_secret")).toMatch(/^[A-Za-z0-9_-]+$/); + + const insertArg = chain.insert.mock.calls[0][0]; + expect(insertArg.code).toBe(json.code); + expect(insertArg.status).toBe("pending"); + expect(insertArg.poll_secret_hash).toMatch(/^[a-f0-9]{64}$/); + expect(insertArg.verify_secret_hash).toMatch(/^[a-f0-9]{64}$/); + expect(insertArg.poll_secret_hash).not.toBe(json.poll_secret); + expect(insertArg.verify_secret_hash).not.toBe(url.searchParams.get("verify_secret")); }); it("returns 500 when insert fails", async () => { @@ -120,13 +141,21 @@ describe("POST /api/auth/cli/poll", () => { expect(json.error).toBe("Missing code"); }); + it("returns error when poll_secret is missing", async () => { + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB" })); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe("Missing poll_secret"); + }); + it("returns expired when code not found", async () => { const chain = mockChain({ single: vi.fn().mockResolvedValue({ data: null, error: { code: "PGRST116" } }), }); mockServiceClient.from.mockReturnValue(chain); - const res = await pollPOST(mockRequest({ code: "XXXX-YYYY" })); + const res = await pollPOST(mockRequest({ code: "XXXX-YYYY", poll_secret: "secret" })); const json = await res.json(); expect(json.status).toBe("expired"); @@ -142,7 +171,7 @@ describe("POST /api/auth/cli/poll", () => { }); mockServiceClient.from.mockReturnValue(chain); - const res = await pollPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB", poll_secret: "secret" })); const json = await res.json(); expect(json.status).toBe("pending"); @@ -158,7 +187,7 @@ describe("POST /api/auth/cli/poll", () => { }); mockServiceClient.from.mockReturnValue(chain); - const res = await pollPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB", poll_secret: "secret" })); const json = await res.json(); expect(json.status).toBe("expired"); @@ -174,7 +203,7 @@ describe("POST /api/auth/cli/poll", () => { }); mockServiceClient.from.mockReturnValue(chain); - const res = await pollPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB", poll_secret: "secret" })); const json = await res.json(); expect(json.status).toBe("expired"); @@ -194,6 +223,10 @@ describe("POST /api/auth/cli/poll", () => { }, error: null, }) + .mockResolvedValueOnce({ + data: { user_id: "user-abc" }, + error: null, + }) .mockResolvedValueOnce({ data: { username: "testuser" }, error: null, @@ -202,13 +235,45 @@ describe("POST /api/auth/cli/poll", () => { (createCliToken as any).mockReturnValue("jwt-token-123"); - const res = await pollPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB", poll_secret: "secret" })); const json = await res.json(); expect(json.status).toBe("completed"); expect(json.token).toBe("jwt-token-123"); expect(json.username).toBe("testuser"); expect(createCliToken).toHaveBeenCalledWith("user-abc", "testuser"); + expect(chain.update).toHaveBeenCalledWith({ + status: "used", + redeemed_at: expect.any(String), + }); + expect(chain.is).toHaveBeenCalledWith("redeemed_at", null); + }); + + it("does not mint a token when a completed code was already redeemed", async () => { + const futureDate = new Date(Date.now() + 600_000).toISOString(); + const chain = mockChain(); + chain.single = vi.fn() + .mockResolvedValueOnce({ + data: { + id: "1", + code: "AAAA-BBBB", + status: "completed", + expires_at: futureDate, + user_id: "user-abc", + }, + error: null, + }) + .mockResolvedValueOnce({ + data: null, + error: { code: "PGRST116", message: "No rows" }, + }); + mockServiceClient.from.mockReturnValue(chain); + + const res = await pollPOST(mockRequest({ code: "AAAA-BBBB", poll_secret: "secret" })); + const json = await res.json(); + + expect(json.status).toBe("expired"); + expect(createCliToken).not.toHaveBeenCalled(); }); }); @@ -216,7 +281,7 @@ describe("POST /api/auth/cli/verify", () => { it("rejects unauthenticated requests", async () => { mockAuthenticatedUser(null); - const res = await verifyPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await verifyPOST(mockRequest({ code: "AAAA-BBBB", verify_secret: "secret" })); const json = await res.json(); expect(res.status).toBe(401); @@ -233,6 +298,16 @@ describe("POST /api/auth/cli/verify", () => { expect(json.error).toBe("Missing code"); }); + it("returns error when verify_secret is missing", async () => { + mockAuthenticatedUser(); + + const res = await verifyPOST(mockRequest({ code: "AAAA-BBBB" })); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe("Missing verify_secret"); + }); + it("returns error for invalid JSON", async () => { mockAuthenticatedUser(); @@ -257,7 +332,7 @@ describe("POST /api/auth/cli/verify", () => { }); mockServiceClient.from.mockReturnValue(chain); - const res = await verifyPOST(mockRequest({ code: "AAAA-BBBB" })); + const res = await verifyPOST(mockRequest({ code: "AAAA-BBBB", verify_secret: "secret" })); const json = await res.json(); expect(res.status).toBe(400); @@ -274,7 +349,7 @@ describe("POST /api/auth/cli/verify", () => { }); mockServiceClient.from.mockReturnValue(chain); - const res = await verifyPOST(mockRequest({ code: " AAAA-BBBB " })); + const res = await verifyPOST(mockRequest({ code: " AAAA-BBBB ", verify_secret: "secret" })); const json = await res.json(); expect(res.status).toBe(200); @@ -284,6 +359,7 @@ describe("POST /api/auth/cli/verify", () => { status: "completed", }); expect(chain.eq).toHaveBeenCalledWith("code", "AAAA-BBBB"); + expect(chain.eq).toHaveBeenCalledWith("verify_secret_hash", expect.stringMatching(/^[a-f0-9]{64}$/)); expect(chain.eq).toHaveBeenCalledWith("status", "pending"); expect(chain.gt).toHaveBeenCalledWith("expires_at", expect.any(String)); expect(chain.select).toHaveBeenCalledWith("id"); diff --git a/apps/web/__tests__/api/comment-email-notifications.test.ts b/apps/web/__tests__/api/comment-email-notifications.test.ts index 54cb2049..69fcd83c 100644 --- a/apps/web/__tests__/api/comment-email-notifications.test.ts +++ b/apps/web/__tests__/api/comment-email-notifications.test.ts @@ -116,6 +116,11 @@ function makeServiceClient({ emailNotifications }: { emailNotifications: boolean }; return { + rpc: vi.fn((fn: string) => Promise.resolve( + fn === "check_rate_limit" + ? { data: [{ allowed: true, retry_after_seconds: 0 }], error: null } + : { data: null, error: null }, + )), from: vi.fn().mockImplementation((table: string) => { if (table === "users") return usersChain; if (table === "posts") return postsChain; diff --git a/apps/web/__tests__/api/social.test.ts b/apps/web/__tests__/api/social.test.ts index 4cb3d111..ec57038b 100644 --- a/apps/web/__tests__/api/social.test.ts +++ b/apps/web/__tests__/api/social.test.ts @@ -4,6 +4,14 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(), })); +const mockServiceClient = { + rpc: vi.fn(), +}; + +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: vi.fn(() => mockServiceClient), +})); + import { POST as followPOST, DELETE as followDELETE } from "@/app/api/follow/[username]/route"; import { POST as kudosPOST, @@ -45,6 +53,10 @@ function makeRequest( beforeEach(() => { vi.clearAllMocks(); + mockServiceClient.rpc.mockResolvedValue({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); }); // ---------- Follow ---------- diff --git a/apps/web/__tests__/api/upload.test.ts b/apps/web/__tests__/api/upload.test.ts index 7fadd13a..77007ade 100644 --- a/apps/web/__tests__/api/upload.test.ts +++ b/apps/web/__tests__/api/upload.test.ts @@ -5,6 +5,14 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(), })); +const mockServiceClient = { + rpc: vi.fn(), +}; + +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: vi.fn(() => mockServiceClient), +})); + // Mock heic-convert so tests don't need real HEIC decoding vi.mock("heic-convert", () => ({ default: vi.fn().mockResolvedValue(new ArrayBuffer(8)), @@ -88,6 +96,10 @@ function makeUploadRequest( beforeEach(() => { vi.clearAllMocks(); resetRateLimiters(); + mockServiceClient.rpc.mockResolvedValue({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); }); describe("POST /api/upload", () => { diff --git a/apps/web/__tests__/api/usage-submit.test.ts b/apps/web/__tests__/api/usage-submit.test.ts index 29c9a785..49b126b3 100644 --- a/apps/web/__tests__/api/usage-submit.test.ts +++ b/apps/web/__tests__/api/usage-submit.test.ts @@ -23,6 +23,14 @@ import { verifyCliToken, verifyCliTokenWithRefresh } from "@/lib/api/cli-auth"; import { getServiceClient } from "@/lib/supabase/service"; import { resetRateLimiters } from "@/lib/rate-limit"; +function mockAllowedRpc() { + return vi.fn((fn: string) => Promise.resolve( + fn === "check_rate_limit" + ? { data: [{ allowed: true, retry_after_seconds: 0 }], error: null } + : { data: null, error: null }, + )); +} + function makeEntry(dateStr: string, overrides: Record = {}) { return { date: dateStr, @@ -53,10 +61,7 @@ function daysAgo(n: number) { function mockServiceClient(overrides: Record = {}) { const chain: Record = { from: vi.fn().mockReturnThis(), - rpc: vi.fn().mockResolvedValue({ - data: null, - error: null, - }), + rpc: mockAllowedRpc(), upsert: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), delete: vi.fn().mockReturnThis(), @@ -233,6 +238,44 @@ describe("POST /api/usage/submit", () => { expect(res.status).toBe(200); expect(json.results).toHaveLength(2); + expect(json.results.map((result: { date: string }) => result.date)).toEqual([ + todayStr(), + daysAgo(1), + ]); + }); + + it("rejects duplicate dates", async () => { + (verifyCliToken as any).mockReturnValue("user-1"); + mockServiceClient(); + const date = todayStr(); + + const res = await POST( + mockRequest({ + entries: [makeEntry(date), makeEntry(date)], + source: "cli", + }) + ); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe(`Duplicate date: ${date}`); + }); + + it("rejects more than 32 entries", async () => { + (verifyCliToken as any).mockReturnValue("user-1"); + mockServiceClient(); + const entries = Array.from({ length: 33 }, (_, index) => makeEntry(daysAgo(index % 31))); + + const res = await POST( + mockRequest({ + entries, + source: "cli", + }) + ); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe("Too many entries provided. Maximum is 32."); }); it("rejects dates older than 30 days", async () => { @@ -416,6 +459,25 @@ describe("POST /api/usage/submit", () => { expect(json.error).toBe("Invalid JSON"); }); + it("rejects request bodies over 256KB", async () => { + (verifyCliToken as any).mockReturnValue("user-1"); + const req = new Request("http://localhost/api/usage/submit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + entries: [makeEntry(todayStr(), { models: ["claude-sonnet-4-5-20250929"], large: "x".repeat(260 * 1024) })], + source: "cli", + device_id: DEFAULT_DEVICE_ID, + }), + }); + + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(413); + expect(json.error).toBe("Request body too large"); + }); + it("rejects empty entries array", async () => { (verifyCliToken as any).mockReturnValue("user-1"); const res = await POST( @@ -674,7 +736,7 @@ describe("POST /api/usage/submit", () => { return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -793,7 +855,7 @@ describe("POST /api/usage/submit", () => { return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const DEVICE_B = "11111111-2222-3333-4444-555555555555"; const res = await POST( @@ -884,7 +946,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -977,7 +1039,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1103,7 +1165,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1213,7 +1275,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1311,7 +1373,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1430,7 +1492,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1569,7 +1631,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ @@ -1675,7 +1737,7 @@ describe("POST /api/usage/submit", () => { if (table === "posts") return postChain; return dailyChain; }); - (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: vi.fn().mockResolvedValue({ data: null, error: null }) }); + (getServiceClient as any).mockReturnValue({ from: fromFn, rpc: mockAllowedRpc() }); const res = await POST( mockRequest({ diff --git a/apps/web/__tests__/flows/cli-push-flow.test.ts b/apps/web/__tests__/flows/cli-push-flow.test.ts index 942a8425..9988427d 100644 --- a/apps/web/__tests__/flows/cli-push-flow.test.ts +++ b/apps/web/__tests__/flows/cli-push-flow.test.ts @@ -12,11 +12,15 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(() => mockSessionClient), })); -vi.mock("@/lib/api/cli-auth", () => ({ - createCliToken: vi.fn(() => "mock-cli-jwt-token"), - verifyCliToken: vi.fn(), - verifyCliTokenWithRefresh: vi.fn(), -})); +vi.mock("@/lib/api/cli-auth", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createCliToken: vi.fn(() => "mock-cli-jwt-token"), + verifyCliToken: vi.fn(), + verifyCliTokenWithRefresh: vi.fn(), + }; +}); const mockServiceClient = { from: vi.fn(), @@ -81,7 +85,12 @@ describe("Flow: CLI Push", () => { vi.useFakeTimers({ now: new Date('2026-03-13T12:00:00Z'), toFake: ['Date'] }); vi.clearAllMocks(); autoDeriveCliAuthMocks(); - mockServiceClient.rpc.mockResolvedValue({ data: null, error: null }); + mockServiceClient.rpc.mockImplementation((fn: string) => { + if (fn === "check_rate_limit") { + return Promise.resolve({ data: [{ allowed: true, retry_after_seconds: 0 }], error: null }); + } + return Promise.resolve({ data: null, error: null }); + }); vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://straude.com"); vi.stubEnv("NEXT_PUBLIC_SUPABASE_URL", "https://test.supabase.co"); vi.stubEnv("SUPABASE_SECRET_KEY", "test-secret"); @@ -112,6 +121,7 @@ describe("Flow: CLI Push", () => { expect(data.code).toBeDefined(); expect(data.code).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/); expect(data.verify_url).toContain("https://straude.com/cli/verify?code="); + expect(data.poll_secret).toBeDefined(); expect(mockServiceClient.from).toHaveBeenCalledWith("cli_auth_codes"); }); @@ -135,7 +145,7 @@ describe("Flow: CLI Push", () => { const req = makeRequest("http://localhost:3000/api/auth/cli/poll", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code: "ABCD-EFGH" }), + body: JSON.stringify({ code: "ABCD-EFGH", poll_secret: "secret" }), }); const res = await POST(req); const data = await res.json(); @@ -166,7 +176,7 @@ describe("Flow: CLI Push", () => { const req = makeRequest("http://localhost:3000/api/auth/cli/poll", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code: "ABCD-EFGH" }), + body: JSON.stringify({ code: "ABCD-EFGH", poll_secret: "secret" }), }); const res = await POST(req); const data = await res.json(); diff --git a/apps/web/__tests__/flows/post-lifecycle.test.ts b/apps/web/__tests__/flows/post-lifecycle.test.ts index 95894fd4..ae8083f3 100644 --- a/apps/web/__tests__/flows/post-lifecycle.test.ts +++ b/apps/web/__tests__/flows/post-lifecycle.test.ts @@ -12,6 +12,10 @@ vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(() => mockSupabase), })); +vi.mock("@/lib/achievements", () => ({ + checkAndAwardAchievements: vi.fn().mockResolvedValue(undefined), +})); + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- diff --git a/apps/web/__tests__/flows/social-interactions.test.ts b/apps/web/__tests__/flows/social-interactions.test.ts index 626bcc8f..2980b69a 100644 --- a/apps/web/__tests__/flows/social-interactions.test.ts +++ b/apps/web/__tests__/flows/social-interactions.test.ts @@ -9,10 +9,18 @@ const mockSupabase = { rpc: vi.fn(), }; +const mockServiceClient = { + rpc: vi.fn(), +}; + vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn(() => mockSupabase), })); +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: vi.fn(() => mockServiceClient), +})); + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -53,6 +61,10 @@ describe("Flow: Social Interactions", () => { beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); + mockServiceClient.rpc.mockResolvedValue({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); }); it("User A follows User B", async () => { diff --git a/apps/web/__tests__/flows/web-import-flow.test.ts b/apps/web/__tests__/flows/web-import-flow.test.ts index 8b9c2715..385eea3a 100644 --- a/apps/web/__tests__/flows/web-import-flow.test.ts +++ b/apps/web/__tests__/flows/web-import-flow.test.ts @@ -82,7 +82,12 @@ describe("Flow: Web JSON Import", () => { beforeEach(() => { vi.clearAllMocks(); - mockServiceClient.rpc.mockResolvedValue({ data: null, error: null }); + mockServiceClient.rpc.mockImplementation((fn: string) => { + if (fn === "check_rate_limit") { + return Promise.resolve({ data: [{ allowed: true, retry_after_seconds: 0 }], error: null }); + } + return Promise.resolve({ data: null, error: null }); + }); vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://straude.com"); vi.stubEnv("NEXT_PUBLIC_SUPABASE_URL", "https://test.supabase.co"); vi.stubEnv("SUPABASE_SECRET_KEY", "test-secret"); diff --git a/apps/web/__tests__/unit/migration-safety.test.ts b/apps/web/__tests__/unit/migration-safety.test.ts index b618a09a..b7c83356 100644 --- a/apps/web/__tests__/unit/migration-safety.test.ts +++ b/apps/web/__tests__/unit/migration-safety.test.ts @@ -183,6 +183,24 @@ describe("Migration safety", () => { expect(/jsonb_build_object\s*\(/i.test(latest.content)).toBe(true); }); + it("latest cli_auth_codes hardening removes public grants and pending-code select policies", () => { + const latest = getLatestMigrationMatching( + migrations, + /public\.cli_auth_codes[\s\S]*(REVOKE\s+ALL|DROP\s+POLICY)/i + ); + + expect(latest, "Expected cli_auth_codes hardening migration").toBeTruthy(); + const content = latest!.content; + + expect(/REVOKE\s+ALL\s+ON\s+TABLE\s+public\.cli_auth_codes\s+FROM\s+anon/i.test(content)).toBe(true); + expect(/REVOKE\s+ALL\s+ON\s+TABLE\s+public\.cli_auth_codes\s+FROM\s+authenticated/i.test(content)).toBe(true); + expect(/GRANT\s+SELECT\s+ON\s+(TABLE\s+)?public\.cli_auth_codes\s+TO\s+anon/i.test(content)).toBe(false); + expect(/GRANT\s+SELECT[^;]+public\.cli_auth_codes[^;]+authenticated/i.test(content)).toBe(false); + expect(/CREATE\s+POLICY[\s\S]*ON\s+(TABLE\s+)?public\.cli_auth_codes[\s\S]*status\s*=\s*'pending'/i.test(content)).toBe(false); + expect(/DROP\s+POLICY\s+IF\s+EXISTS\s+"Users can view own cli auth codes"/i.test(content)).toBe(true); + expect(/DROP\s+POLICY\s+IF\s+EXISTS\s+"Authenticated users can verify pending codes"/i.test(content)).toBe(true); + }); + it("latest interaction RLS policies inherit parent post visibility", () => { // Match only migrations that actually CREATE POLICY on the interaction // tables — not every migration that happens to reference them in passing diff --git a/apps/web/__tests__/unit/rate-limit.test.ts b/apps/web/__tests__/unit/rate-limit.test.ts new file mode 100644 index 00000000..186c5eea --- /dev/null +++ b/apps/web/__tests__/unit/rate-limit.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const mockRpc = vi.fn(); + +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: vi.fn(() => ({ rpc: mockRpc })), +})); + +import { rateLimit } from "@/lib/rate-limit"; + +describe("rateLimit", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns null when the RPC allows the request", async () => { + mockRpc.mockResolvedValueOnce({ + data: [{ allowed: true, retry_after_seconds: 0 }], + error: null, + }); + + const limited = await rateLimit("upload", "user-1", { limit: 10 }); + + expect(limited).toBeNull(); + expect(mockRpc).toHaveBeenCalledWith("check_rate_limit", { + p_name: "upload", + p_subject: "user-1", + p_limit: 10, + p_window_seconds: 60, + }); + }); + + it("returns 429 with Retry-After when the RPC denies the request", async () => { + mockRpc.mockResolvedValueOnce({ + data: [{ allowed: false, retry_after_seconds: 17 }], + error: null, + }); + + const limited = await rateLimit("ai-caption:minute", "user-1", { + limit: 5, + windowSeconds: 60, + }); + + expect(limited?.status).toBe(429); + expect(limited?.headers.get("Retry-After")).toBe("17"); + await expect(limited?.json()).resolves.toEqual({ + error: "Too many requests. Please try again later.", + }); + }); + + it("returns 503 when the RPC fails", async () => { + mockRpc.mockResolvedValueOnce({ + data: null, + error: { message: "database unavailable" }, + }); + + const limited = await rateLimit("upload", "user-1", { limit: 10 }); + + expect(limited?.status).toBe(503); + await expect(limited?.json()).resolves.toEqual({ + error: "Rate limit check failed", + }); + }); +}); diff --git a/apps/web/app/api/ai/generate-caption/route.ts b/apps/web/app/api/ai/generate-caption/route.ts index 6b6e5201..bba19c1f 100644 --- a/apps/web/app/api/ai/generate-caption/route.ts +++ b/apps/web/app/api/ai/generate-caption/route.ts @@ -1,7 +1,18 @@ import { NextResponse, type NextRequest } from "next/server"; import { createClient } from "@/lib/supabase/server"; +import { rateLimit } from "@/lib/rate-limit"; +import { isFirstPartyPublicStorageUrl } from "@/lib/storage"; import Anthropic from "@anthropic-ai/sdk"; +interface CaptionUsage { + costUSD?: number; + totalTokens?: number; + inputTokens?: number; + outputTokens?: number; + models?: string[]; + sessionCount?: number; +} + export async function POST(request: NextRequest) { const supabase = await createClient(); const { @@ -12,7 +23,14 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { images, usage } = await request.json(); + let body: { images?: unknown; usage?: CaptionUsage }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + const { images, usage } = body; if (!images || !Array.isArray(images) || images.length === 0) { return NextResponse.json( @@ -28,26 +46,26 @@ export async function POST(request: NextRequest) { ); } - // Only allow images hosted on our own Supabase storage to prevent SSRF - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null; for (const url of images) { if (typeof url !== "string") { return NextResponse.json({ error: "Invalid image URL" }, { status: 400 }); } - try { - const parsed = new URL(url); - if (!allowedOrigin || parsed.origin !== allowedOrigin) { - return NextResponse.json( - { error: "Images must be hosted on Straude storage" }, - { status: 400 } - ); - } - } catch { - return NextResponse.json({ error: "Invalid image URL" }, { status: 400 }); + if (!isFirstPartyPublicStorageUrl(url, "post-images")) { + return NextResponse.json( + { error: "Images must use Straude post image uploads" }, + { status: 400 } + ); } } + const minuteLimited = await rateLimit("ai-caption:minute", user.id, { limit: 5 }); + if (minuteLimited) return minuteLimited; + const dayLimited = await rateLimit("ai-caption:day", user.id, { + limit: 50, + windowSeconds: 24 * 60 * 60, + }); + if (dayLimited) return dayLimited; + const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { return NextResponse.json( diff --git a/apps/web/app/api/auth/cli/init/route.ts b/apps/web/app/api/auth/cli/init/route.ts index 60b43dca..a7481b56 100644 --- a/apps/web/app/api/auth/cli/init/route.ts +++ b/apps/web/app/api/auth/cli/init/route.ts @@ -1,24 +1,9 @@ import { NextResponse, type NextRequest } from "next/server"; import { randomBytes } from "node:crypto"; +import { createCliDeviceSecret, hashCliDeviceSecret } from "@/lib/api/cli-auth"; +import { rateLimit } from "@/lib/rate-limit"; import { getServiceClient } from "@/lib/supabase/service"; -// Simple in-process rate limiter: max 5 requests per IP per minute -const rateLimitMap = new Map(); -const RATE_LIMIT = 5; -const RATE_WINDOW_MS = 60_000; - -function checkRateLimit(ip: string): boolean { - const now = Date.now(); - const entry = rateLimitMap.get(ip); - if (!entry || now > entry.resetAt) { - rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS }); - return true; - } - if (entry.count >= RATE_LIMIT) return false; - entry.count++; - return true; -} - function generateCode(): string { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I/O/0/1 for readability let part1 = ""; @@ -37,19 +22,19 @@ export async function POST(request: NextRequest) { request.headers.get("x-real-ip") ?? "unknown"; - if (!checkRateLimit(ip)) { - return NextResponse.json( - { error: "Too many requests. Please wait before trying again." }, - { status: 429 } - ); - } + const limited = await rateLimit("cli-auth-init", ip, { limit: 5 }); + if (limited) return limited; const supabase = getServiceClient(); const code = generateCode(); + const pollSecret = createCliDeviceSecret(); + const verifySecret = createCliDeviceSecret(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString(); const { error } = await supabase.from("cli_auth_codes").insert({ code, + poll_secret_hash: hashCliDeviceSecret(pollSecret), + verify_secret_hash: hashCliDeviceSecret(verifySecret), status: "pending", expires_at: expiresAt, }); @@ -58,8 +43,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Failed to create auth code" }, { status: 500 }); } - const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com"; - const verifyUrl = `${appUrl}/cli/verify?code=${code}`; + const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com").replace(/\/+$/, ""); + const params = new URLSearchParams({ code, verify_secret: verifySecret }); + const verifyUrl = `${appUrl}/cli/verify?${params.toString()}`; - return NextResponse.json({ code, verify_url: verifyUrl }); + return NextResponse.json({ code, verify_url: verifyUrl, poll_secret: pollSecret }); } diff --git a/apps/web/app/api/auth/cli/poll/route.ts b/apps/web/app/api/auth/cli/poll/route.ts index 4e80de65..eca1288f 100644 --- a/apps/web/app/api/auth/cli/poll/route.ts +++ b/apps/web/app/api/auth/cli/poll/route.ts @@ -1,26 +1,32 @@ import { NextResponse } from "next/server"; -import { createCliToken } from "@/lib/api/cli-auth"; +import { createCliToken, hashCliDeviceSecret } from "@/lib/api/cli-auth"; import { getServiceClient } from "@/lib/supabase/service"; export async function POST(request: Request) { - let body: { code?: string }; + let body: { code?: unknown; poll_secret?: unknown }; try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } - const { code } = body; + const code = typeof body.code === "string" ? body.code.trim() : ""; if (!code) { return NextResponse.json({ error: "Missing code" }, { status: 400 }); } + const pollSecret = typeof body.poll_secret === "string" ? body.poll_secret.trim() : ""; + if (!pollSecret) { + return NextResponse.json({ error: "Missing poll_secret" }, { status: 400 }); + } const supabase = getServiceClient(); + const pollSecretHash = hashCliDeviceSecret(pollSecret); const { data: authCode, error } = await supabase .from("cli_auth_codes") .select("*") .eq("code", code) + .eq("poll_secret_hash", pollSecretHash) .neq("status", "expired") .single(); @@ -30,12 +36,12 @@ export async function POST(request: Request) { // Check if expired if (new Date(authCode.expires_at) < new Date()) { - // Mark as expired if still pending - if (authCode.status === "pending") { + if (authCode.status === "pending" || authCode.status === "completed") { await supabase .from("cli_auth_codes") .update({ status: "expired" }) - .eq("id", authCode.id); + .eq("id", authCode.id) + .eq("poll_secret_hash", pollSecretHash); } return NextResponse.json({ status: "expired" }); } @@ -49,14 +55,29 @@ export async function POST(request: Request) { } if (authCode.status === "completed" && authCode.user_id) { + const redeemedAt = new Date().toISOString(); + const { data: redeemed, error: redeemError } = await supabase + .from("cli_auth_codes") + .update({ status: "used", redeemed_at: redeemedAt }) + .eq("id", authCode.id) + .eq("status", "completed") + .eq("poll_secret_hash", pollSecretHash) + .is("redeemed_at", null) + .select("user_id") + .single(); + + if (redeemError || !redeemed?.user_id) { + return NextResponse.json({ status: "expired" }); + } + // Fetch username for the token payload const { data: user } = await supabase .from("users") .select("username") - .eq("id", authCode.user_id) + .eq("id", redeemed.user_id) .single(); - const token = createCliToken(authCode.user_id, user?.username ?? null); + const token = createCliToken(redeemed.user_id, user?.username ?? null); return NextResponse.json({ status: "completed", diff --git a/apps/web/app/api/auth/cli/verify/route.ts b/apps/web/app/api/auth/cli/verify/route.ts index e8e43cc5..659a9f5c 100644 --- a/apps/web/app/api/auth/cli/verify/route.ts +++ b/apps/web/app/api/auth/cli/verify/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { hashCliDeviceSecret } from "@/lib/api/cli-auth"; import { getServiceClient } from "@/lib/supabase/service"; import { createClient } from "@/lib/supabase/server"; @@ -12,7 +13,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - let body: { code?: unknown }; + let body: { code?: unknown; verify_secret?: unknown }; try { body = await request.json(); } catch { @@ -23,12 +24,17 @@ export async function POST(request: Request) { if (!code) { return NextResponse.json({ error: "Missing code" }, { status: 400 }); } + const verifySecret = typeof body.verify_secret === "string" ? body.verify_secret.trim() : ""; + if (!verifySecret) { + return NextResponse.json({ error: "Missing verify_secret" }, { status: 400 }); + } const db = getServiceClient(); const { data, error } = await db .from("cli_auth_codes") .update({ user_id: user.id, status: "completed" }) .eq("code", code) + .eq("verify_secret_hash", hashCliDeviceSecret(verifySecret)) .eq("status", "pending") .gt("expires_at", new Date().toISOString()) .select("id") diff --git a/apps/web/app/api/comments/[id]/reactions/route.ts b/apps/web/app/api/comments/[id]/reactions/route.ts index ab986b3d..e35b2be4 100644 --- a/apps/web/app/api/comments/[id]/reactions/route.ts +++ b/apps/web/app/api/comments/[id]/reactions/route.ts @@ -15,7 +15,7 @@ export async function POST(_request: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const { data: comment, error: commentError } = await supabase diff --git a/apps/web/app/api/comments/[id]/route.ts b/apps/web/app/api/comments/[id]/route.ts index 6fb3b683..d77e6969 100644 --- a/apps/web/app/api/comments/[id]/route.ts +++ b/apps/web/app/api/comments/[id]/route.ts @@ -15,7 +15,7 @@ export async function PATCH(request: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const { content } = await request.json(); diff --git a/apps/web/app/api/follow/[username]/route.ts b/apps/web/app/api/follow/[username]/route.ts index 5520e83c..ea77407e 100644 --- a/apps/web/app/api/follow/[username]/route.ts +++ b/apps/web/app/api/follow/[username]/route.ts @@ -16,7 +16,7 @@ export async function POST(_request: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const { data: target } = await supabase diff --git a/apps/web/app/api/messages/route.ts b/apps/web/app/api/messages/route.ts index 11158d55..bcd0e7f8 100644 --- a/apps/web/app/api/messages/route.ts +++ b/apps/web/app/api/messages/route.ts @@ -237,7 +237,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const { recipientUsername, content, attachments: rawAttachments } = await request.json(); diff --git a/apps/web/app/api/posts/[id]/comments/route.ts b/apps/web/app/api/posts/[id]/comments/route.ts index 4900b001..128935fb 100644 --- a/apps/web/app/api/posts/[id]/comments/route.ts +++ b/apps/web/app/api/posts/[id]/comments/route.ts @@ -84,7 +84,7 @@ export async function POST(request: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const body = await request.json(); diff --git a/apps/web/app/api/posts/[id]/kudos/route.ts b/apps/web/app/api/posts/[id]/kudos/route.ts index abcffc6c..3491ed8d 100644 --- a/apps/web/app/api/posts/[id]/kudos/route.ts +++ b/apps/web/app/api/posts/[id]/kudos/route.ts @@ -17,7 +17,7 @@ export async function POST(_request: NextRequest, context: RouteContext) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("social", user.id, { limit: 30 }); + const limited = await rateLimit("social", user.id, { limit: 30 }); if (limited) return limited; const { data: post, error: postError } = await supabase diff --git a/apps/web/app/api/upload/route.ts b/apps/web/app/api/upload/route.ts index 0ab7b13d..c9a1144a 100644 --- a/apps/web/app/api/upload/route.ts +++ b/apps/web/app/api/upload/route.ts @@ -114,7 +114,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const limited = rateLimit("upload", user.id, { limit: 10 }); + const limited = await rateLimit("upload", user.id, { limit: 10 }); if (limited) return limited; const bucketParam = request.nextUrl.searchParams.get("bucket") ?? "post-images"; diff --git a/apps/web/app/api/usage/submit/route.ts b/apps/web/app/api/usage/submit/route.ts index b7e2b387..85ceb0ae 100644 --- a/apps/web/app/api/usage/submit/route.ts +++ b/apps/web/app/api/usage/submit/route.ts @@ -8,6 +8,9 @@ import { formatCurrency } from "@/lib/utils/format"; import type { UsageSubmitRequest, UsageSubmitResponse, CcusageDailyEntry, ModelBreakdownEntry, UsageCollectorMeta } from "@/types"; const MAX_BACKFILL_DAYS = 30; +const MAX_USAGE_ENTRIES = MAX_BACKFILL_DAYS + 2; +const MAX_USAGE_BODY_BYTES = 256 * 1024; +const USAGE_PROCESS_CONCURRENCY = 4; // Trusted collectors are the only ones allowed to *lower* Codex totals on // UPSERT, which is how the server accepts retroactive collector corrections. const TRUSTED_CODEX_COLLECTORS = new Set([ @@ -74,6 +77,98 @@ function validateCollectorMeta(collector: UsageCollectorMeta | undefined): strin return null; } +type JsonReadResult = + | { ok: true; body: T } + | { ok: false; response: NextResponse }; + +async function readJsonBodyWithLimit( + request: Request, + maxBytes: number, +): Promise> { + const contentLength = request.headers.get("content-length"); + if (contentLength) { + const parsedLength = Number(contentLength); + if (Number.isFinite(parsedLength) && parsedLength > maxBytes) { + return { + ok: false, + response: NextResponse.json({ error: "Request body too large" }, { status: 413 }), + }; + } + } + + if (!request.body) { + return { + ok: false, + response: NextResponse.json({ error: "Invalid JSON" }, { status: 400 }), + }; + } + + const reader = request.body.getReader(); + const chunks: Uint8Array[] = []; + let totalBytes = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!value) continue; + + totalBytes += value.byteLength; + if (totalBytes > maxBytes) { + return { + ok: false, + response: NextResponse.json({ error: "Request body too large" }, { status: 413 }), + }; + } + chunks.push(value); + } + + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + try { + return { + ok: true, + body: JSON.parse(new TextDecoder().decode(bytes)) as T, + }; + } catch { + return { + ok: false, + response: NextResponse.json({ error: "Invalid JSON" }, { status: 400 }), + }; + } +} + +async function mapSettledWithConcurrency( + items: T[], + concurrency: number, + mapper: (item: T, index: number) => Promise, +): Promise[]> { + const results = new Array>(items.length); + let nextIndex = 0; + + async function worker() { + while (nextIndex < items.length) { + const index = nextIndex; + nextIndex += 1; + + try { + results[index] = { status: "fulfilled", value: await mapper(items[index]!, index) }; + } catch (reason) { + results[index] = { status: "rejected", reason }; + } + } + } + + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, () => worker()), + ); + return results; +} + function isCodexModel(model: unknown): boolean { return typeof model === "string" && CODEX_MODEL_RE.test(model); } @@ -335,16 +430,19 @@ export function aggregateDeviceRows(rows: DeviceUsageRow[]) { } export async function POST(request: Request) { - let body: UsageSubmitRequest; - try { - body = await request.json(); - } catch { - return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); - } + const parsed = await readJsonBodyWithLimit(request, MAX_USAGE_BODY_BYTES); + if (!parsed.ok) return parsed.response; + const body = parsed.body; if (!body.entries || !Array.isArray(body.entries) || body.entries.length === 0) { return NextResponse.json({ error: "No entries provided" }, { status: 400 }); } + if (body.entries.length > MAX_USAGE_ENTRIES) { + return NextResponse.json( + { error: `Too many entries provided. Maximum is ${MAX_USAGE_ENTRIES}.` }, + { status: 400 }, + ); + } if (!body.source || !["cli", "web"].includes(body.source)) { return NextResponse.json({ error: "Invalid source" }, { status: 400 }); @@ -362,14 +460,18 @@ export async function POST(request: Request) { const userId = auth.userId; - const limited = rateLimit("usage-submit", userId, { limit: 20 }); + const limited = await rateLimit("usage-submit", userId, { limit: 20 }); if (limited) return limited; - // Validate all entries + const seenDates = new Set(); for (const entry of body.entries) { if (!isValidDate(entry.date)) { return NextResponse.json({ error: `Invalid date: ${entry.date}` }, { status: 400 }); } + if (seenDates.has(entry.date)) { + return NextResponse.json({ error: `Duplicate date: ${entry.date}` }, { status: 400 }); + } + seenDates.add(entry.date); if (!isWithinBackfillWindow(entry.date, MAX_BACKFILL_DAYS)) { return NextResponse.json( { error: `Date ${entry.date} is outside the ${MAX_BACKFILL_DAYS}-day backfill window` }, @@ -398,9 +500,10 @@ export async function POST(request: Request) { ); } - // Process all entries concurrently — each entry is independent per-date - const settled = await Promise.allSettled( - body.entries.map(async (entry) => { + const settled = await mapSettledWithConcurrency( + body.entries, + USAGE_PROCESS_CONCURRENCY, + async (entry) => { // Check if a record already exists to determine create vs update const { data: existing } = await db .from("daily_usage") @@ -674,7 +777,7 @@ export async function POST(request: Request) { daily_total: agg.cost_usd, device_count: deviceRows.length, }; - }), + }, ); // Collect results and errors diff --git a/apps/web/app/cli/verify/page.tsx b/apps/web/app/cli/verify/page.tsx index d1055def..540faf4a 100644 --- a/apps/web/app/cli/verify/page.tsx +++ b/apps/web/app/cli/verify/page.tsx @@ -14,21 +14,26 @@ function VerifyContent() { const searchParams = useSearchParams(); const router = useRouter(); const code = searchParams.get("code"); + const verifySecret = searchParams.get("verify_secret"); + const verifyPath = `/cli/verify?${new URLSearchParams({ + ...(code ? { code } : {}), + ...(verifySecret ? { verify_secret: verifySecret } : {}), + }).toString()}`; const [state, setState] = useState<"idle" | "loading" | "success" | "error" | "unauthenticated">("idle"); const [errorMsg, setErrorMsg] = useState(""); // Check auth on mount — redirect early if not logged in useEffect(() => { - if (!code) return; + if (!code || !verifySecret) return; const supabase = createClient(); supabase.auth.getUser().then(({ data: { user } }) => { if (!user) { setState("unauthenticated"); } }); - }, [code]); + }, [code, verifySecret]); - if (!code) { + if (!code || !verifySecret) { return (

No authorization code provided.

@@ -45,18 +50,18 @@ function VerifyContent() { const { data: { user }, error: authError } = await supabase.auth.getUser(); if (authError || !user) { - router.push(`/login?next=${encodeURIComponent(`/cli/verify?code=${code}`)}`); + router.push(`/login?next=${encodeURIComponent(verifyPath)}`); return; } const verifyRes = await fetch("/api/auth/cli/verify", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code }), + body: JSON.stringify({ code, verify_secret: verifySecret }), }); if (verifyRes.status === 401) { - router.push(`/login?next=${encodeURIComponent(`/cli/verify?code=${code}`)}`); + router.push(`/login?next=${encodeURIComponent(verifyPath)}`); return; } @@ -75,7 +80,7 @@ function VerifyContent() { } function handleSignIn() { - router.push(`/login?next=${encodeURIComponent(`/cli/verify?code=${code}`)}`); + router.push(`/login?next=${encodeURIComponent(verifyPath)}`); } return ( diff --git a/apps/web/e2e/golden-path/cli-verify.spec.ts b/apps/web/e2e/golden-path/cli-verify.spec.ts index ac3a2468..1017d90a 100644 --- a/apps/web/e2e/golden-path/cli-verify.spec.ts +++ b/apps/web/e2e/golden-path/cli-verify.spec.ts @@ -1,8 +1,12 @@ import { test, expect } from "@playwright/test"; +function verifyUrl(code: string) { + return `/cli/verify?code=${encodeURIComponent(code)}&verify_secret=test-verify-secret`; +} + test.describe("CLI verify page", () => { test("page loads with authorization heading", async ({ page }) => { - await page.goto("/cli/verify?code=TEST1234"); + await page.goto(verifyUrl("TEST1234")); await page.waitForLoadState("networkidle"); await expect(page.locator("h1")).toContainText("Authorize CLI"); @@ -10,7 +14,7 @@ test.describe("CLI verify page", () => { test("displays the authorization code from URL", async ({ page }) => { const testCode = "ABCD1234"; - await page.goto(`/cli/verify?code=${testCode}`); + await page.goto(verifyUrl(testCode)); await page.waitForLoadState("networkidle"); await expect(page.getByText(testCode)).toBeVisible(); @@ -22,7 +26,7 @@ test.describe("CLI verify page", () => { test("unauthenticated user sees sign-in or authorize action", async ({ page, }) => { - await page.goto("/cli/verify?code=TEST1234"); + await page.goto(verifyUrl("TEST1234")); await page.waitForLoadState("networkidle"); // Either sign-in button (for guests) or authorize button (for logged-in) @@ -35,7 +39,7 @@ test.describe("CLI verify page", () => { }); test("sign-in link includes return URL", async ({ page }) => { - await page.goto("/cli/verify?code=TEST1234"); + await page.goto(verifyUrl("TEST1234")); await page.waitForLoadState("networkidle"); const signInLink = page.locator('a:has-text("Sign in to authorize")'); diff --git a/apps/web/lib/api/cli-auth.ts b/apps/web/lib/api/cli-auth.ts index 2f6d485b..e65ebead 100644 --- a/apps/web/lib/api/cli-auth.ts +++ b/apps/web/lib/api/cli-auth.ts @@ -1,4 +1,4 @@ -import { createHmac, timingSafeEqual } from "node:crypto"; +import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; interface JwtPayload { sub: string; @@ -22,6 +22,14 @@ export interface CliAuthResult { refreshedToken: string | null; } +export function createCliDeviceSecret(): string { + return randomBytes(32).toString("base64url"); +} + +export function hashCliDeviceSecret(secret: string): string { + return createHash("sha256").update(secret, "utf8").digest("hex"); +} + function base64urlEncode(data: string): string { return Buffer.from(data, "utf-8") .toString("base64") diff --git a/apps/web/lib/rate-limit.ts b/apps/web/lib/rate-limit.ts index e9e29ab6..71de2abe 100644 --- a/apps/web/lib/rate-limit.ts +++ b/apps/web/lib/rate-limit.ts @@ -1,11 +1,11 @@ import { NextResponse } from "next/server"; +import { getServiceClient } from "@/lib/supabase/service"; /** - * In-memory sliding window rate limiter keyed by user ID. + * Durable fixed-window rate limiter backed by Supabase. * - * Each limiter instance maintains its own request map, so different - * endpoints can have independent limits. Timestamps older than the - * window are pruned on every check to bound memory growth. + * The database function owns the atomic counter update so limits are shared + * across serverless instances instead of resetting with each process. */ interface RateLimitConfig { @@ -15,65 +15,15 @@ interface RateLimitConfig { windowSeconds?: number; } -interface CheckResult { +interface RateLimitRpcResult { allowed: boolean; - /** Seconds until the earliest request in the window expires. */ - retryAfterSeconds: number; -} - -class RateLimiter { - private requests = new Map(); - private readonly limit: number; - private readonly windowMs: number; - - constructor(config: RateLimitConfig) { - this.limit = config.limit; - this.windowMs = (config.windowSeconds ?? 60) * 1000; - } - - check(userId: string): CheckResult { - const now = Date.now(); - const windowStart = now - this.windowMs; - - let timestamps = this.requests.get(userId); - if (timestamps) { - // Prune expired entries - timestamps = timestamps.filter((t) => t > windowStart); - this.requests.set(userId, timestamps); - } else { - timestamps = []; - } - - if (timestamps.length >= this.limit) { - const oldestInWindow = timestamps[0]!; - const retryAfterMs = oldestInWindow + this.windowMs - now; - return { - allowed: false, - retryAfterSeconds: Math.ceil(retryAfterMs / 1000), - }; - } - - timestamps.push(now); - this.requests.set(userId, timestamps); - return { allowed: true, retryAfterSeconds: 0 }; - } -} - -// Shared limiter instances (one per endpoint group) -const limiters = new Map(); - -function getLimiter(name: string, config: RateLimitConfig): RateLimiter { - let limiter = limiters.get(name); - if (!limiter) { - limiter = new RateLimiter(config); - limiters.set(name, limiter); - } - return limiter; + retry_after_seconds: number; } /** Reset all limiter state. Useful in tests. */ export function resetRateLimiters(): void { - limiters.clear(); + // Durable limiter state lives in Supabase. Tests mock the service client + // instead of clearing production state. } /** @@ -82,24 +32,42 @@ export function resetRateLimiters(): void { * * Usage: * ```ts - * const limited = rateLimit("upload", userId, { limit: 10 }); + * const limited = await rateLimit("upload", userId, { limit: 10 }); * if (limited) return limited; * ``` */ -export function rateLimit( +export async function rateLimit( name: string, - userId: string, + subject: string, config: RateLimitConfig, -): NextResponse | null { - const limiter = getLimiter(name, config); - const result = limiter.check(userId); +): Promise { + const db = getServiceClient(); + const { data, error } = await db.rpc("check_rate_limit", { + p_name: name, + p_subject: subject, + p_limit: config.limit, + p_window_seconds: config.windowSeconds ?? 60, + }); + + if (error) { + return NextResponse.json( + { error: "Rate limit check failed" }, + { status: 503 }, + ); + } + + const row = Array.isArray(data) + ? (data[0] as RateLimitRpcResult | undefined) + : (data as RateLimitRpcResult | null); + const allowed = row?.allowed ?? false; + const retryAfterSeconds = Math.max(1, row?.retry_after_seconds ?? config.windowSeconds ?? 60); - if (!result.allowed) { + if (!allowed) { return NextResponse.json( { error: "Too many requests. Please try again later." }, { status: 429, - headers: { "Retry-After": String(result.retryAfterSeconds) }, + headers: { "Retry-After": String(retryAfterSeconds) }, }, ); } diff --git a/apps/web/package.json b/apps/web/package.json index f47c8efb..c65b01bf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,7 +22,7 @@ "@react-email/components": "^1.0.8", "@straude/shared": "workspace:*", "@supabase/ssr": "^0.6", - "@supabase/supabase-js": "^2", + "@supabase/supabase-js": "2.108.1", "@tanstack/react-query": "^5.100.5", "@vercel/analytics": "^1.6.1", "agentation": "^2.2.1", @@ -33,13 +33,13 @@ "lucide-react": "^0.474", "motion": "^12", "next": "16.2.6", - "posthog-js": "1.359.1", + "posthog-js": "1.383.3", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^9", "recharts": "^3.7.0", "remark-breaks": "^4.0.0", - "resend": "^6.9.2", + "resend": "6.12.4", "sharp": "^0.34.5", "tailwind-merge": "^3" }, @@ -53,13 +53,13 @@ "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", - "@vitejs/plugin-react": "^5.1.4", - "eslint": "^9", + "@vitejs/plugin-react": "^5.2.0", + "eslint": "^9.39.4", "eslint-config-next": "16.2.6", "jsdom": "^28.1.0", "pg": "^8.20.0", "tailwindcss": "^4", "typescript": "5.9.3", - "vitest": "^4.0.18" + "vitest": "^4.1.8" } } diff --git a/bun.lock b/bun.lock index 8ab8ac04..56db0c87 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "web": "^0.0.2", }, "devDependencies": { - "turbo": "^2.9.6", + "turbo": "^2.9.17", "typescript": "^5", }, }, @@ -24,7 +24,7 @@ "@react-email/components": "^1.0.8", "@straude/shared": "workspace:*", "@supabase/ssr": "^0.6", - "@supabase/supabase-js": "^2", + "@supabase/supabase-js": "2.108.1", "@tanstack/react-query": "^5.100.5", "@vercel/analytics": "^1.6.1", "agentation": "^2.2.1", @@ -35,13 +35,13 @@ "lucide-react": "^0.474", "motion": "^12", "next": "16.2.6", - "posthog-js": "1.359.1", + "posthog-js": "1.383.3", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^9", "recharts": "^3.7.0", "remark-breaks": "^4.0.0", - "resend": "^6.9.2", + "resend": "6.12.4", "sharp": "^0.34.5", "tailwind-merge": "^3", }, @@ -55,14 +55,14 @@ "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", - "@vitejs/plugin-react": "^5.1.4", - "eslint": "^9", + "@vitejs/plugin-react": "^5.2.0", + "eslint": "^9.39.4", "eslint-config-next": "16.2.6", "jsdom": "^28.1.0", "pg": "^8.20.0", "tailwindcss": "^4", "typescript": "5.9.3", - "vitest": "^4.0.18", + "vitest": "^4.1.8", }, }, "packages/cli": { @@ -85,7 +85,7 @@ "@types/react": "^19.2.14", "tsup": "^8.5.1", "typescript": "^5", - "vitest": "^4.0.18", + "vitest": "^4.1.8", }, }, "packages/shared": { @@ -98,6 +98,16 @@ }, "overrides": { "@21st-sdk/node": "0.0.9", + "brace-expansion": "5.0.6", + "dompurify": "3.4.8", + "flatted": "3.4.2", + "minimatch": "10.2.5", + "picomatch": "4.0.4", + "postcss": "8.5.15", + "rollup": "4.61.1", + "undici": "7.25.0", + "vite": "8.0.16", + "ws": "8.21.0", }, "packages": { "@21st-sdk/agent": ["@21st-sdk/agent@0.0.15", "", { "dependencies": { "@21st-sdk/node": "workspace:^" }, "peerDependencies": { "zod": ">=3.24" } }, "sha512-GQDT00zxNYX+F3MJ06hqj65K/UmcVoKMf0TbFfWoFg5B2r2laqesyrJWERRqVsb8iU7dlvdBQx1fv0otVyQEAA=="], @@ -190,11 +200,11 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], @@ -252,15 +262,15 @@ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], @@ -348,7 +358,7 @@ "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], @@ -380,54 +390,16 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - - "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], - - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], - - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], - - "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], - - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], - "@posthog/core": ["@posthog/core@1.23.2", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ=="], + "@posthog/core": ["@posthog/core@1.30.14", "", { "dependencies": { "@posthog/types": "1.383.3" } }, "sha512-cC0che/17kP6qMIMgdmxsoz3h8Jar8knQfDM8WqQwVacSeWXkrwkemoV7S5tCGmgTuRTTsdigirs9HiBXHQ/dA=="], - "@posthog/types": ["@posthog/types@1.359.1", "", {}, "sha512-oQihoHWLnOkSkzOToCWKNigbJ7UZcIkl+rSJuq2PLwL7EB0Q/r1UGSbVCkrPH8xtPbYpi7w4TVpMrg41TMT+LQ=="], + "@posthog/types": ["@posthog/types@1.383.3", "", {}, "sha512-N4jtmLaJxzjQ/C0UHnF0igQPSwUqwScPDv9ePGjKCfDomIEUcO3+c6pBrjTp7woxuMQ49BmyM/pV4/SOBPpe0Q=="], "@pppp606/ink-chart": ["@pppp606/ink-chart@0.2.4", "", { "peerDependencies": { "ink": ">=6", "react": ">=19" }, "bin": { "ink-chart-demo": "bin/demo.js" } }, "sha512-iqzTFePJKgzypEMdziIIo/uRDtQGBnunWxsvmaHTIBBxQIK0sRDoOszJzDc0UNGRxM+bOSPtuC844sqH/lJorg=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w=="], @@ -484,57 +456,87 @@ "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -550,19 +552,21 @@ "@straude/web": ["@straude/web@workspace:apps/web"], - "@supabase/auth-js": ["@supabase/auth-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg=="], + "@supabase/auth-js": ["@supabase/auth-js@2.108.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Lle5rKU8f9LF3K5dDd8Or8mkkG+ptzRZZWKPVMm9B9UuovH65Ss2+iFnQqRsCqaGouvJEcTWyl0cj2riNrrDLQ=="], + + "@supabase/functions-js": ["@supabase/functions-js@2.108.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-fxBRW/A4IG7ADQztVt0NaEy5ysiO1WJ2pbldsnBchrkHuyepX0Krek9qA9T4gUQBVVTCE9Ea4pdsM5hfn3nc4A=="], - "@supabase/functions-js": ["@supabase/functions-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig=="], + "@supabase/phoenix": ["@supabase/phoenix@0.4.2", "", {}, "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A=="], - "@supabase/postgrest-js": ["@supabase/postgrest-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg=="], + "@supabase/postgrest-js": ["@supabase/postgrest-js@2.108.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-9lj2MCPPMgSTaJ5y+amnhb3TWPtMFVlbDn2hmX/VV91xQU4j0AauwfMaBErHBJ+zzsSwjc0jLU+zLIZFLQzfig=="], - "@supabase/realtime-js": ["@supabase/realtime-js@2.95.3", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw=="], + "@supabase/realtime-js": ["@supabase/realtime-js@2.108.1", "", { "dependencies": { "@supabase/phoenix": "^0.4.2", "tslib": "2.8.1" } }, "sha512-mHGGqOjwd1XTydcoffUqEMsbFQHUi6A3uhQ0EXr3iqzpLqItxKA9nbN6gIQxrZ7JRRnuUe/iOFPUkYV9Tdc5lg=="], "@supabase/ssr": ["@supabase/ssr@0.6.1", "", { "dependencies": { "cookie": "^1.0.1" }, "peerDependencies": { "@supabase/supabase-js": "^2.43.4" } }, "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g=="], - "@supabase/storage-js": ["@supabase/storage-js@2.95.3", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A=="], + "@supabase/storage-js": ["@supabase/storage-js@2.108.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-Er0SGGt85iT6ye+SSh98Az6L2CesoZJuyzEZYH2oBOAnIxa9Nn4CtwUC3veGxYggoT56X+3tVuuQeDBP8kR8sg=="], - "@supabase/supabase-js": ["@supabase/supabase-js@2.95.3", "", { "dependencies": { "@supabase/auth-js": "2.95.3", "@supabase/functions-js": "2.95.3", "@supabase/postgrest-js": "2.95.3", "@supabase/realtime-js": "2.95.3", "@supabase/storage-js": "2.95.3" } }, "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg=="], + "@supabase/supabase-js": ["@supabase/supabase-js@2.108.1", "", { "dependencies": { "@supabase/auth-js": "2.108.1", "@supabase/functions-js": "2.108.1", "@supabase/postgrest-js": "2.108.1", "@supabase/realtime-js": "2.108.1", "@supabase/storage-js": "2.108.1" } }, "sha512-V/1hRKLSCJ0zEL+9QFRBUtivvePfOsaAYQmC0HhFNSHC2F3xFs4jSF3YhkLmzex6E4V4FGvmBDOP72D/53NnZA=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -606,17 +610,17 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-io5jn5RDeU+9YV78rWhwG++HD/OZ/Lxg1sg93+jDGKQNP3UDxY6RX2dmarbCILhNxNuAM8FH3WgGMY9E96Mf8w=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-83YZTYmN2sxFWf2LTMOwqbOvR3qZMa/TSFwnB6BHVBbIWyoPPe+TAdSTd8KevEx8ml8KkycJ/9A70DFVReyUww=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.17", "", { "os": "linux", "cpu": "x64" }, "sha512-teKfwJg0zSC+C2ZSOsX3VnAJGVgcN+pgKNmnGWzcpXQ9eIkAQtYP+getrQ2f1Tw/ePudnreQhq8tVP8S73Vy6Q=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-mqO36x2CNtJ9CCbEf5xOqH662tVSc1wB0mxh7dopBpgXg0fEifdzwX0IEAUW1WUAaNH986L7iEUUgw3HNqUWgg=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.17", "", { "os": "win32", "cpu": "x64" }, "sha512-jbyoNePufyMoSSrvVr+/mglcjmya/MOgoIrSHPr67iZ1VFgrlMQXHXtptR2lR48gi+86b1XBvsviJZ9A7zrydg=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-+ql0wYc99Y2AMvyHCcC/P+xtyV4nz522L+C9HDnyi7ryHXBGM+ZjBP28M7SLBGMDImgpN8sk2szpgbvreMeXVA=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -676,8 +680,6 @@ "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], - "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -688,8 +690,6 @@ "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="], @@ -752,21 +752,21 @@ "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + "@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="], - "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + "@vitest/runner": ["@vitest/runner@4.1.8", "", { "dependencies": { "@vitest/utils": "4.1.8", "pathe": "^2.0.3" } }, "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ=="], - "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "@vitest/spy": ["@vitest/spy@4.1.8", "", {}, "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA=="], - "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -780,7 +780,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -828,13 +828,13 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -894,8 +894,6 @@ "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -984,7 +982,7 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -1008,7 +1006,7 @@ "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], @@ -1026,7 +1024,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], "eslint-config-next": ["eslint-config-next@16.2.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q=="], @@ -1100,7 +1098,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], @@ -1354,8 +1352,6 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1448,7 +1444,7 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1464,7 +1460,7 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], @@ -1544,7 +1540,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], @@ -1558,9 +1554,9 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postal-mime": ["postal-mime@2.7.3", "", {}, "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw=="], + "postal-mime": ["postal-mime@2.7.4", "", {}, "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], @@ -1572,7 +1568,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js": ["posthog-js@1.359.1", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.23.2", "@posthog/types": "1.359.1", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-Gy/eX02im6ON0zMxfTR61GNk1sjgLT9rVGfBQ5C757/WS4mN3vTUJveQYoX9jr3y0pqPZ57DqCcf6zcw++bpzQ=="], + "posthog-js": ["posthog-js@1.383.3", "", { "dependencies": { "@posthog/core": "1.30.14", "@posthog/types": "1.383.3", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-caH+lSELdgqaS2uxmBiJT22/cr3epuMKSqBsO8QYQgORehokwdWB/Q4LIh3Dgyjy26B2yhprcAAqmuKMf4s3Nw=="], "posthog-node": ["posthog-node@5.29.1", "", { "dependencies": { "@posthog/core": "1.25.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-XA1OGrE8SAmO3JkfWjeVg7XxEN/6M6ZXzYmBJKl3uXv/xxk7ru0oeoqi/rcWJ6z5+nFP9wUFVoS37//qaPxwFg=="], @@ -1590,8 +1586,6 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], @@ -1638,7 +1632,7 @@ "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resend": ["resend@6.9.2", "", { "dependencies": { "postal-mime": "2.7.3", "svix": "1.84.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA=="], + "resend": ["resend@6.12.4", "", { "dependencies": { "postal-mime": "2.7.4", "standardwebhooks": "1.0.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-lRpJ2Hxd+ht+JPDm97juRcUp9HOMuZyxaRFRFmc9Tx8iNWiei94Dx9v6SWufgKk2667C/uCeKKspMotOHSpCSg=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -1652,7 +1646,9 @@ "robot3": ["robot3@0.4.1", "", {}, "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + + "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1712,7 +1708,7 @@ "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -1754,8 +1750,6 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svix": ["svix@1.84.1", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], @@ -1782,7 +1776,7 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="], @@ -1810,7 +1804,7 @@ "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "turbo": ["turbo@2.9.6", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.6", "@turbo/darwin-arm64": "2.9.6", "@turbo/linux-64": "2.9.6", "@turbo/linux-arm64": "2.9.6", "@turbo/windows-64": "2.9.6", "@turbo/windows-arm64": "2.9.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg=="], + "turbo": ["turbo@2.9.17", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.17", "@turbo/darwin-arm64": "2.9.17", "@turbo/linux-64": "2.9.17", "@turbo/linux-arm64": "2.9.17", "@turbo/windows-64": "2.9.17", "@turbo/windows-arm64": "2.9.17" }, "bin": { "turbo": "bin/turbo" } }, "sha512-91Q3KxfHJn7esFu2Ic6j9pkvQqWjncQCOp7r1gCKChRSb/+T/yIjsavAmbGLmFRKAzSjmWW/FMrcknmJ4hEOPA=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -1832,7 +1826,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1856,17 +1850,15 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.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", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], - "vitest": ["vitest@4.0.18", "", { "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", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@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", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "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": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], @@ -1900,7 +1892,7 @@ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -1932,18 +1924,10 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], - - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -1962,10 +1946,10 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], + "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1990,12 +1974,8 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -2008,6 +1988,10 @@ "redent/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "rollup/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], @@ -2022,11 +2006,15 @@ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "vite/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2034,6 +2022,30 @@ "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], } } diff --git a/package.json b/package.json index 01221645..a617bb0c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "local:setup": "bun run local:up && bun run local:env && bun run local:seed" }, "devDependencies": { - "turbo": "^2.9.6", + "turbo": "^2.9.17", "typescript": "^5" }, "dependencies": { @@ -27,6 +27,16 @@ "web": "^0.0.2" }, "overrides": { - "@21st-sdk/node": "0.0.9" + "@21st-sdk/node": "0.0.9", + "brace-expansion": "5.0.6", + "dompurify": "3.4.8", + "flatted": "3.4.2", + "minimatch": "10.2.5", + "picomatch": "4.0.4", + "postcss": "8.5.15", + "rollup": "4.61.1", + "undici": "7.25.0", + "vite": "8.0.16", + "ws": "8.21.0" } } diff --git a/packages/cli/__tests__/commands/login.test.ts b/packages/cli/__tests__/commands/login.test.ts index 545ec5ea..1caac53a 100644 --- a/packages/cli/__tests__/commands/login.test.ts +++ b/packages/cli/__tests__/commands/login.test.ts @@ -61,6 +61,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "completed", token: "tok-123", username: "alice" }); @@ -84,6 +85,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "pending" }) .mockResolvedValueOnce({ status: "pending" }) @@ -94,6 +96,13 @@ describe("loginCommand", () => { expect(mockSaveConfig).toHaveBeenCalledWith( expect.objectContaining({ token: "tok-456", username: "bob" }), ); + expect(mockApiRequestNoAuth).toHaveBeenCalledWith( + "https://straude.com", + "/api/auth/cli/poll", + expect.objectContaining({ + body: JSON.stringify({ code: "ABCD-EFGH", poll_secret: "poll-secret-123" }), + }), + ); }); it("handles expired code", async () => { @@ -101,6 +110,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "expired" }); @@ -118,6 +128,18 @@ describe("loginCommand", () => { expect(process.exit).toHaveBeenCalledWith(1); }); + it("rejects init responses without poll_secret", async () => { + mockApiRequestNoAuth.mockResolvedValueOnce({ + code: "ABCD-EFGH", + verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + }); + + await expect(loginCommand("https://straude.com")).rejects.toThrow(ExitError); + + expect(process.exit).toHaveBeenCalledWith(1); + expect(mockSaveConfig).not.toHaveBeenCalled(); + }); + it("preserves config fields when re-logging into the same account", async () => { mockLoadConfig.mockReturnValueOnce({ token: "old-tok", @@ -131,6 +153,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "completed", token: "new-tok", username: "alice" }); @@ -159,6 +182,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://straude.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "completed", token: "new-tok", username: "bob" }); @@ -187,6 +211,7 @@ describe("loginCommand", () => { .mockResolvedValueOnce({ code: "ABCD-EFGH", verify_url: "https://other.com/cli/verify?code=ABCD-EFGH", + poll_secret: "poll-secret-123", }) .mockResolvedValueOnce({ status: "completed", token: "new-tok", username: "alice" }); diff --git a/packages/cli/package.json b/packages/cli/package.json index 9604bf6d..2f1a111b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,7 +26,7 @@ "@types/react": "^19.2.14", "tsup": "^8.5.1", "typescript": "^5", - "vitest": "^4.0.18" + "vitest": "^4.1.8" }, "dependencies": { "@pppp606/ink-chart": "^0.2.4", diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 710dd74e..61c510b3 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -8,6 +8,7 @@ import { getDistinctId, getMachineId } from "../lib/machine-id.js"; interface CliInitResponse { code: string; verify_url: string; + poll_secret: string; } interface CliPollResponse { @@ -76,7 +77,11 @@ export async function loginCommand(apiUrlOverride?: string): Promise { process.exit(1); } - const { code, verify_url } = initRes; + const { code, verify_url, poll_secret } = initRes; + if (!poll_secret) { + console.error("Failed to start login: server did not return a poll secret. Please update Straude and try again."); + process.exit(1); + } openBrowser(verify_url); console.log(`\nIf the browser didn't open, visit:\n ${verify_url}\n`); @@ -91,7 +96,7 @@ export async function loginCommand(apiUrlOverride?: string): Promise { try { pollRes = await apiRequestNoAuth(apiUrl, "/api/auth/cli/poll", { method: "POST", - body: JSON.stringify({ code }), + body: JSON.stringify({ code, poll_secret }), }); } catch { // Network errors during polling are transient, keep trying diff --git a/supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql b/supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql new file mode 100644 index 00000000..bcb24772 --- /dev/null +++ b/supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql @@ -0,0 +1,136 @@ +-- Security sweep: close CLI device-flow token hijacking and add durable API rate limits. + +-- Expire any currently in-flight login codes because the protocol now requires +-- per-flow secrets that older rows do not have. +UPDATE public.cli_auth_codes +SET status = 'expired' +WHERE status IN ('pending', 'completed') + AND expires_at > now(); + +ALTER TABLE public.cli_auth_codes + ADD COLUMN IF NOT EXISTS poll_secret_hash text, + ADD COLUMN IF NOT EXISTS verify_secret_hash text, + ADD COLUMN IF NOT EXISTS redeemed_at timestamptz; + +ALTER TABLE public.cli_auth_codes + DROP CONSTRAINT IF EXISTS cli_auth_codes_status_check; + +ALTER TABLE public.cli_auth_codes + ADD CONSTRAINT cli_auth_codes_status_check + CHECK (status IN ('pending', 'completed', 'expired', 'used')); + +ALTER TABLE public.cli_auth_codes + DROP CONSTRAINT IF EXISTS cli_auth_codes_active_secrets_check; + +ALTER TABLE public.cli_auth_codes + ADD CONSTRAINT cli_auth_codes_active_secrets_check + CHECK ( + status = 'expired' + OR (poll_secret_hash IS NOT NULL AND verify_secret_hash IS NOT NULL) + ); + +DROP POLICY IF EXISTS "Service role manages cli auth codes" ON public.cli_auth_codes; +DROP POLICY IF EXISTS "Users can view own cli auth codes" ON public.cli_auth_codes; +DROP POLICY IF EXISTS "Authenticated users can verify pending codes" ON public.cli_auth_codes; + +REVOKE ALL ON TABLE public.cli_auth_codes FROM anon; +REVOKE ALL ON TABLE public.cli_auth_codes FROM authenticated; +GRANT ALL ON TABLE public.cli_auth_codes TO service_role; + +CREATE TABLE IF NOT EXISTS public.api_rate_limits ( + name text NOT NULL, + subject text NOT NULL, + window_start timestamptz NOT NULL, + count integer NOT NULL DEFAULT 0 CHECK (count >= 0), + expires_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (name, subject) +); + +ALTER TABLE public.api_rate_limits ENABLE ROW LEVEL SECURITY; + +REVOKE ALL ON TABLE public.api_rate_limits FROM anon; +REVOKE ALL ON TABLE public.api_rate_limits FROM authenticated; +GRANT ALL ON TABLE public.api_rate_limits TO service_role; + +CREATE INDEX IF NOT EXISTS idx_api_rate_limits_expires_at + ON public.api_rate_limits(expires_at); + +CREATE OR REPLACE FUNCTION public.check_rate_limit( + p_name text, + p_subject text, + p_limit integer, + p_window_seconds integer DEFAULT 60 +) +RETURNS TABLE(allowed boolean, retry_after_seconds integer) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, pg_temp +AS $$ +DECLARE + v_now timestamptz := now(); + v_window interval; + v_count integer; + v_expires_at timestamptz; +BEGIN + IF p_name IS NULL OR length(p_name) = 0 + OR p_subject IS NULL OR length(p_subject) = 0 + OR p_limit IS NULL OR p_limit <= 0 + OR p_window_seconds IS NULL OR p_window_seconds <= 0 + THEN + RETURN QUERY SELECT false, 60; + RETURN; + END IF; + + v_window := make_interval(secs => p_window_seconds); + + DELETE FROM public.api_rate_limits + WHERE expires_at < v_now - interval '1 hour'; + + INSERT INTO public.api_rate_limits AS rl ( + name, + subject, + window_start, + count, + expires_at, + updated_at + ) + VALUES ( + p_name, + p_subject, + v_now, + 1, + v_now + v_window, + v_now + ) + ON CONFLICT (name, subject) DO UPDATE + SET + count = CASE + WHEN rl.expires_at <= v_now THEN 1 + ELSE rl.count + 1 + END, + window_start = CASE + WHEN rl.expires_at <= v_now THEN v_now + ELSE rl.window_start + END, + expires_at = CASE + WHEN rl.expires_at <= v_now THEN v_now + v_window + ELSE rl.expires_at + END, + updated_at = v_now + RETURNING rl.count, rl.expires_at + INTO v_count, v_expires_at; + + RETURN QUERY SELECT + v_count <= p_limit, + CASE + WHEN v_count <= p_limit THEN 0 + ELSE greatest(1, ceil(extract(epoch FROM (v_expires_at - v_now)))::integer) + END; +END; +$$; + +REVOKE ALL ON FUNCTION public.check_rate_limit(text, text, integer, integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION public.check_rate_limit(text, text, integer, integer) FROM anon; +REVOKE ALL ON FUNCTION public.check_rate_limit(text, text, integer, integer) FROM authenticated; +GRANT EXECUTE ON FUNCTION public.check_rate_limit(text, text, integer, integer) TO service_role; diff --git a/supabase/migrations/20260703000000_grant_service_role_usage_tables.sql b/supabase/migrations/20260703000000_grant_service_role_usage_tables.sql new file mode 100644 index 00000000..80172806 --- /dev/null +++ b/supabase/migrations/20260703000000_grant_service_role_usage_tables.sql @@ -0,0 +1,15 @@ +-- Grant the server-side service role explicit write access to the tables that +-- POST /api/usage/submit writes through the service client. +-- +-- These tables (daily_usage, device_usage, posts) were only ever GRANTed to +-- `authenticated`; service_role's access relied on Postgres default +-- privileges. Newer local Supabase images enforce table-level GRANTs for +-- service_role instead of falling back to those defaults, so the usage-submit +-- route started returning `permission denied for table device_usage` (500) +-- against a freshly-booted `supabase start` stack — breaking the real-Supabase +-- integration test. In hosted Supabase service_role already holds these +-- privileges, so these statements are idempotent no-ops there. + +GRANT SELECT, INSERT, UPDATE ON public.daily_usage TO service_role; +GRANT SELECT, INSERT, UPDATE ON public.device_usage TO service_role; +GRANT SELECT, INSERT, UPDATE, DELETE ON public.posts TO service_role;