Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/api/subscriptions/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getSubscriptionStatusHandler } from "@/lib/stripe/getSubscriptionStatusHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A 200 NextResponse carrying the CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* GET /api/subscriptions/status?accountId=
*
* Returns whether the account has an active paid subscription (direct or via organization).
* Requires `x-api-key` or `Authorization: Bearer`; the caller must be allowed to access
* the requested account (same rules as account_id override on other routes).
*
* @param request - The incoming HTTP request (query `accountId`, auth headers).
* @returns JSON `{ isPro }`, or 400/401/403 with `{ error }` per API docs.
*/
export async function GET(request: NextRequest) {
return getSubscriptionStatusHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
2 changes: 1 addition & 1 deletion lib/sandbox/__tests__/createSandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createSandbox } from "../createSandbox";
import { Sandbox } from "@vercel/sandbox";

const mockSandbox = {
name: "sbx_test123",
sandboxId: "sbx_test123",
status: "running",
timeout: 1800000,
createdAt: new Date("2024-01-01T00:00:00Z"),
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/__tests__/createSandboxFromSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ vi.mock("@/lib/supabase/account_sandboxes/insertAccountSandbox", () => ({

describe("createSandboxFromSnapshot", () => {
const mockSandbox = {
name: "sbx_new",
sandboxId: "sbx_new",
status: "running",
runCommand: vi.fn(),
} as unknown as Sandbox;
Expand Down
6 changes: 3 additions & 3 deletions lib/sandbox/__tests__/getActiveSandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("getActiveSandbox", () => {
mockSelectAccountSandboxes.mockResolvedValue([{ sandbox_id: "sbx_123", account_id: "acc_1" }]);

const mockSandbox = {
name: "sbx_123",
sandboxId: "sbx_123",
status: "running",
runCommand: vi.fn(),
};
Expand All @@ -35,7 +35,7 @@ describe("getActiveSandbox", () => {
expect(mockSelectAccountSandboxes).toHaveBeenCalledWith({
accountIds: ["acc_1"],
});
expect(Sandbox.get).toHaveBeenCalledWith({ name: "sbx_123" });
expect(Sandbox.get).toHaveBeenCalledWith({ sandboxId: "sbx_123" });
expect(result).toBe(mockSandbox);
});

Expand All @@ -54,7 +54,7 @@ describe("getActiveSandbox", () => {
]);

const mockSandbox = {
name: "sbx_stopped",
sandboxId: "sbx_stopped",
status: "stopped",
};
vi.mocked(Sandbox.get).mockResolvedValue(mockSandbox as unknown as Sandbox);
Expand Down
6 changes: 3 additions & 3 deletions lib/sandbox/__tests__/getOrCreateSandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("getOrCreateSandbox", () => {

it("returns existing sandbox with created=false and fromSnapshot=true", async () => {
const mockSandbox = {
name: "sbx_existing",
sandboxId: "sbx_existing",
status: "running",
} as unknown as Sandbox;

Expand All @@ -40,7 +40,7 @@ describe("getOrCreateSandbox", () => {

it("creates new sandbox from snapshot with created=true, fromSnapshot=true", async () => {
const mockSandbox = {
name: "sbx_new",
sandboxId: "sbx_new",
status: "running",
} as unknown as Sandbox;

Expand All @@ -63,7 +63,7 @@ describe("getOrCreateSandbox", () => {

it("creates fresh sandbox with created=true, fromSnapshot=false", async () => {
const mockSandbox = {
name: "sbx_fresh",
sandboxId: "sbx_fresh",
status: "running",
} as unknown as Sandbox;

Expand Down
8 changes: 4 additions & 4 deletions lib/sandbox/__tests__/getSandboxStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("getSandboxStatus", () => {

it("returns sandbox status when sandbox exists", async () => {
const mockSandbox = {
name: "sbx_123",
sandboxId: "sbx_123",
status: "running",
timeout: 600000,
createdAt: new Date("2024-01-01T00:00:00.000Z"),
Expand All @@ -25,7 +25,7 @@ describe("getSandboxStatus", () => {

const result = await getSandboxStatus("sbx_123");

expect(Sandbox.get).toHaveBeenCalledWith({ name: "sbx_123" });
expect(Sandbox.get).toHaveBeenCalledWith({ sandboxId: "sbx_123" });
expect(result).toEqual({
sandboxId: "sbx_123",
sandboxStatus: "running",
Expand All @@ -52,7 +52,7 @@ describe("getSandboxStatus", () => {

it("handles stopped sandbox status", async () => {
const mockSandbox = {
name: "sbx_stopped",
sandboxId: "sbx_stopped",
status: "stopped",
timeout: 0,
createdAt: new Date("2024-01-01T00:00:00.000Z"),
Expand All @@ -71,7 +71,7 @@ describe("getSandboxStatus", () => {

it("handles pending sandbox status", async () => {
const mockSandbox = {
name: "sbx_pending",
sandboxId: "sbx_pending",
status: "pending",
timeout: 600000,
createdAt: new Date("2024-01-01T00:00:00.000Z"),
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/__tests__/processCreateSandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ vi.mock("@/lib/trigger/triggerPromptSandbox", () => ({
}));

const mockSandbox = {
name: "sbx_123",
sandboxId: "sbx_123",
status: "running",
timeout: 600000,
createdAt: new Date("2024-01-01T00:00:00.000Z"),
Expand Down
10 changes: 6 additions & 4 deletions lib/sandbox/createSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import ms from "ms";
import { Sandbox } from "@vercel/sandbox";

type VercelSandbox = InstanceType<typeof Sandbox>;

export interface SandboxCreatedResponse {
sandboxId: Sandbox["name"];
sandboxStatus: Sandbox["status"];
timeout: Sandbox["timeout"];
sandboxId: VercelSandbox["sandboxId"];
sandboxStatus: VercelSandbox["status"];
timeout: VercelSandbox["timeout"];
createdAt: string;
}

Expand Down Expand Up @@ -54,7 +56,7 @@ export async function createSandbox(
return {
sandbox,
response: {
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
sandboxStatus: sandbox.status,
timeout: sandbox.timeout,
createdAt: sandbox.createdAt.toISOString(),
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/createSandboxFromSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function createSandboxFromSnapshot(

await insertAccountSandbox({
account_id: accountId,
sandbox_id: sandbox.name,
sandbox_id: sandbox.sandboxId,
});

return { sandbox, fromSnapshot };
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/getActiveSandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function getActiveSandbox(accountId: string): Promise<Sandbox | nul
const mostRecent = sandboxes[0];

try {
const sandbox = await Sandbox.get({ name: mostRecent.sandbox_id });
const sandbox = await Sandbox.get({ sandboxId: mostRecent.sandbox_id });

if (sandbox.status === "running") {
return sandbox;
Expand Down
4 changes: 2 additions & 2 deletions lib/sandbox/getOrCreateSandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function getOrCreateSandbox(accountId: string): Promise<GetOrCreate
if (existing) {
return {
sandbox: existing,
sandboxId: existing.name,
sandboxId: existing.sandboxId,
created: false,
fromSnapshot: true,
};
Expand All @@ -31,7 +31,7 @@ export async function getOrCreateSandbox(accountId: string): Promise<GetOrCreate

return {
sandbox,
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
created: true,
fromSnapshot,
};
Expand Down
4 changes: 2 additions & 2 deletions lib/sandbox/getSandboxStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import type { SandboxCreatedResponse } from "./createSandbox";
*/
export async function getSandboxStatus(sandboxId: string): Promise<SandboxCreatedResponse | null> {
try {
const sandbox = await Sandbox.get({ name: sandboxId });
const sandbox = await Sandbox.get({ sandboxId });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Use the beta SDK's name field here; this pinned version expects Sandbox.get({ name }).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/getSandboxStatus.ts, line 12:

<comment>Use the beta SDK's `name` field here; this pinned version expects `Sandbox.get({ name })`.</comment>

<file context>
@@ -9,10 +9,10 @@ import type { SandboxCreatedResponse } from "./createSandbox";
 export async function getSandboxStatus(sandboxId: string): Promise<SandboxCreatedResponse | null> {
   try {
-    const sandbox = await Sandbox.get({ name: sandboxId });
+    const sandbox = await Sandbox.get({ sandboxId });
 
     return {
</file context>
Suggested change
const sandbox = await Sandbox.get({ sandboxId });
const sandbox = await Sandbox.get({ name: sandboxId });


return {
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Read sandbox.name here; this beta SDK renamed the identifier field.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/getSandboxStatus.ts, line 15:

<comment>Read `sandbox.name` here; this beta SDK renamed the identifier field.</comment>

<file context>
@@ -9,10 +9,10 @@ import type { SandboxCreatedResponse } from "./createSandbox";
 
     return {
-      sandboxId: sandbox.name,
+      sandboxId: sandbox.sandboxId,
       sandboxStatus: sandbox.status,
       timeout: sandbox.timeout,
</file context>
Suggested change
sandboxId: sandbox.sandboxId,
sandboxId: sandbox.name,

sandboxStatus: sandbox.status,
timeout: sandbox.timeout,
createdAt: sandbox.createdAt.toISOString(),
Expand Down
4 changes: 2 additions & 2 deletions lib/sandbox/processCreateSandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function processCreateSandbox(
const { sandbox } = await createSandboxFromSnapshot(accountId);

const result: SandboxCreatedResponse = {
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
sandboxStatus: sandbox.status,
timeout: sandbox.timeout,
createdAt: sandbox.createdAt.toISOString(),
Expand All @@ -35,7 +35,7 @@ export async function processCreateSandbox(
try {
const handle = await triggerPromptSandbox({
prompt,
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
accountId,
});
runId = handle.id;
Expand Down
69 changes: 69 additions & 0 deletions lib/stripe/__tests__/getSubscriptionStatusHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { NextRequest, NextResponse } from "next/server";
import { getSubscriptionStatusHandler } from "@/lib/stripe/getSubscriptionStatusHandler";
import { validateGetSubscriptionStatusRequest } from "@/lib/stripe/validateGetSubscriptionStatusRequest";
import { getSubscriptionIsPro } from "@/lib/stripe/getSubscriptionIsPro";

vi.mock("@/lib/networking/getCorsHeaders", () => ({
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
}));

vi.mock("@/lib/stripe/validateGetSubscriptionStatusRequest", () => ({
validateGetSubscriptionStatusRequest: vi.fn(),
}));

vi.mock("@/lib/stripe/getSubscriptionIsPro", () => ({
getSubscriptionIsPro: vi.fn(),
}));

const ACCOUNT = "123e4567-e89b-12d3-a456-426614174000";

describe("getSubscriptionStatusHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, "error").mockImplementation(() => undefined);
});
afterEach(() => vi.mocked(console.error).mockRestore());

it("returns validation response unchanged", async () => {
const err = NextResponse.json({ error: "accountId is required" }, { status: 400 });
vi.mocked(validateGetSubscriptionStatusRequest).mockResolvedValue(err);
const req = new NextRequest(`http://localhost/api/subscriptions/status`);
expect(await getSubscriptionStatusHandler(req)).toBe(err);
expect(getSubscriptionIsPro).not.toHaveBeenCalled();
});

it("returns 200 { isPro: true }", async () => {
vi.mocked(validateGetSubscriptionStatusRequest).mockResolvedValue({ accountId: ACCOUNT });
vi.mocked(getSubscriptionIsPro).mockResolvedValue(true);

const res = await getSubscriptionStatusHandler(
new NextRequest(`http://localhost/api/subscriptions/status?accountId=${ACCOUNT}`),
);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ isPro: true });
expect(getSubscriptionIsPro).toHaveBeenCalledWith(ACCOUNT);
});

it("returns 200 { isPro: false }", async () => {
vi.mocked(validateGetSubscriptionStatusRequest).mockResolvedValue({ accountId: ACCOUNT });
vi.mocked(getSubscriptionIsPro).mockResolvedValue(false);

const res = await getSubscriptionStatusHandler(
new NextRequest(`http://localhost/api/subscriptions/status?accountId=${ACCOUNT}`),
);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ isPro: false });
});

it("returns 500 when getSubscriptionIsPro throws", async () => {
vi.mocked(validateGetSubscriptionStatusRequest).mockResolvedValue({ accountId: ACCOUNT });
vi.mocked(getSubscriptionIsPro).mockRejectedValue(new Error("stripe down"));

const res = await getSubscriptionStatusHandler(
new NextRequest(`http://localhost/api/subscriptions/status?accountId=${ACCOUNT}`),
);
expect(res.status).toBe(500);
await expect(res.json()).resolves.toEqual({ error: "Internal server error" });
});
});
Loading
Loading