From e9248fe0c671746ad4e5a618f4e533072cc25c4f Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 16 Mar 2026 15:52:45 -0700 Subject: [PATCH 01/37] chore: move sql migrations into infra repo --- migrations/20250205180205.sql | 109 ------------------ migrations/20250311144556.sql | 10 -- migrations/20250314133234.sql | 14 --- migrations/20260212120822.sql | 206 ---------------------------------- migrations/20260217145145.sql | 107 ------------------ 5 files changed, 446 deletions(-) delete mode 100644 migrations/20250205180205.sql delete mode 100644 migrations/20250311144556.sql delete mode 100644 migrations/20250314133234.sql delete mode 100644 migrations/20260212120822.sql delete mode 100644 migrations/20260217145145.sql diff --git a/migrations/20250205180205.sql b/migrations/20250205180205.sql deleted file mode 100644 index 3b3a95061..000000000 --- a/migrations/20250205180205.sql +++ /dev/null @@ -1,109 +0,0 @@ -/* -This migration adds team slugs and profile pictures to support user-friendly URLs and team branding. - -It performs the following steps: - -1. Adds two new columns to the teams table: - - slug: A URL-friendly version of the team name (e.g. "acme-inc") - - profile_picture_url: URL to the team's profile picture - -2. Creates a slug generation function that: - - Takes a team name and converts it to a URL-friendly format - - Removes special characters, accents, and spaces - - Handles email addresses by only using the part before @ - - Converts to lowercase and replaces spaces with hyphens - -3. Installs the unaccent PostgreSQL extension for proper accent handling - -4. Generates initial slugs for all existing teams: - - Uses the team name as base for the slug - - If multiple teams would have the same slug, appends part of the team ID - to ensure uniqueness - -5. Sets up automatic slug generation for new teams: - - Creates a trigger that runs before team insertion - - Generates a unique slug using random suffixes if needed - - Only generates a slug if one isn't explicitly provided - -6. Enforces slug uniqueness with a database constraint -*/ - -ALTER TABLE teams -ADD COLUMN slug TEXT, -ADD COLUMN profile_picture_url TEXT; - -CREATE OR REPLACE FUNCTION generate_team_slug(name TEXT) -RETURNS TEXT AS $$ -DECLARE - base_name TEXT; -BEGIN - base_name := SPLIT_PART(name, '@', 1); - - RETURN LOWER( - REGEXP_REPLACE( - REGEXP_REPLACE( - UNACCENT(TRIM(base_name)), - '[^a-zA-Z0-9\s-]', - '', - 'g' - ), - '\s+', - '-', - 'g' - ) - ); -END; -$$ LANGUAGE plpgsql; - -CREATE EXTENSION IF NOT EXISTS unaccent; - -WITH numbered_teams AS ( - SELECT - id, - name, - generate_team_slug(name) as base_slug, - ROW_NUMBER() OVER (PARTITION BY generate_team_slug(name) ORDER BY created_at) as slug_count - FROM teams - WHERE slug IS NULL -) -UPDATE teams -SET slug = - CASE - WHEN t.slug_count = 1 THEN t.base_slug - ELSE t.base_slug || '-' || SUBSTRING(teams.id::text, 1, 4) - END -FROM numbered_teams t -WHERE teams.id = t.id; - -CREATE OR REPLACE FUNCTION generate_team_slug_trigger() -RETURNS TRIGGER AS $$ -DECLARE - base_slug TEXT; - test_slug TEXT; - suffix TEXT; -BEGIN - IF NEW.slug IS NULL THEN - base_slug := generate_team_slug(NEW.name); - test_slug := base_slug; - - WHILE EXISTS (SELECT 1 FROM teams WHERE slug = test_slug) LOOP - suffix := SUBSTRING(gen_random_uuid()::text, 1, 4); - test_slug := base_slug || '-' || suffix; - END LOOP; - - NEW.slug := test_slug; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER team_slug_trigger -BEFORE INSERT ON teams -FOR EACH ROW -EXECUTE FUNCTION generate_team_slug_trigger(); - -ALTER TABLE teams -ADD CONSTRAINT teams_slug_unique UNIQUE (slug); - -ALTER TABLE teams -ALTER COLUMN slug SET NOT NULL; \ No newline at end of file diff --git a/migrations/20250311144556.sql b/migrations/20250311144556.sql deleted file mode 100644 index 16c4649f3..000000000 --- a/migrations/20250311144556.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE OR REPLACE VIEW public.auth_users AS -SELECT - id, - email -FROM auth.users; - --- Revoke all permissions to ensure no public access -REVOKE ALL ON public.auth_users FROM PUBLIC; -REVOKE ALL ON public.auth_users FROM anon; -REVOKE ALL ON public.auth_users FROM authenticated; \ No newline at end of file diff --git a/migrations/20250314133234.sql b/migrations/20250314133234.sql deleted file mode 100644 index 0ce0801e8..000000000 --- a/migrations/20250314133234.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Timestamp: 20250314133234 - --- Create env_defaults table with RLS enabled -CREATE TABLE "public"."env_defaults" ( - env_id TEXT PRIMARY KEY REFERENCES "public"."envs"(id), - description TEXT -); - --- Enable Row Level Security -ALTER TABLE "public"."env_defaults" ENABLE ROW LEVEL SECURITY; - --- Create an index on the foreign key for better performance -CREATE INDEX "env_defaults_env_id_idx" ON "public"."env_defaults"("env_id"); - diff --git a/migrations/20260212120822.sql b/migrations/20260212120822.sql deleted file mode 100644 index 7962bcd09..000000000 --- a/migrations/20260212120822.sql +++ /dev/null @@ -1,206 +0,0 @@ --- Timestamp: 20260212120822 - -BEGIN; - -CREATE OR REPLACE FUNCTION public.list_team_builds_rpc( - p_team_id uuid, - p_statuses text[] DEFAULT ARRAY['waiting', 'building', 'uploaded', 'failed']::text[], - p_limit integer DEFAULT 50, - p_cursor_created_at timestamptz DEFAULT NULL, - p_cursor_id uuid DEFAULT NULL, - p_build_id_or_template text DEFAULT NULL -) -RETURNS TABLE ( - id uuid, - status text, - reason jsonb, - created_at timestamptz, - finished_at timestamptz, - template_id text, - template_alias text -) -LANGUAGE sql -STABLE -SECURITY INVOKER -SET search_path = pg_catalog, public -AS $function$ -WITH params AS ( - SELECT - p_team_id AS team_id, - CASE - WHEN p_statuses IS NULL OR CARDINALITY(p_statuses) = 0 - THEN ARRAY['waiting', 'building', 'uploaded', 'failed']::text[] - ELSE p_statuses - END AS statuses, - GREATEST(1, LEAST(COALESCE(p_limit, 50), 100)) AS requested_limit, - p_cursor_created_at AS cursor_created_at, - p_cursor_id AS cursor_id, - NULLIF(BTRIM(p_build_id_or_template), '') AS search_term -), -resolved AS ( - SELECT - p.*, - CASE - WHEN p.search_term ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' - THEN p.search_term::uuid - ELSE NULL - END AS candidate_build_id, - ( - SELECT e.id - FROM public.envs e - WHERE e.team_id = p.team_id - AND e.id = p.search_term - LIMIT 1 - ) AS resolved_template_id_by_id - FROM params p -), -resolved_with_alias AS ( - SELECT - r.*, - COALESCE( - r.resolved_template_id_by_id, - ( - SELECT ea.env_id - FROM public.env_aliases ea - JOIN public.envs e ON e.id = ea.env_id - WHERE e.team_id = r.team_id - AND ea.alias = r.search_term - ORDER BY ea.id ASC - LIMIT 1 - ) - ) AS resolved_template_id - FROM resolved r -), -page_ids AS ( - SELECT DISTINCT ON (b.created_at, b.id) - b.id, - b.created_at, - a.env_id - FROM resolved_with_alias f - JOIN public.envs e - ON e.team_id = f.team_id - JOIN public.env_build_assignments a - ON a.env_id = e.id - JOIN public.env_builds b - ON b.id = a.build_id - WHERE b.status = ANY (f.statuses) - AND ( - f.cursor_created_at IS NULL - OR (f.cursor_id IS NULL AND b.created_at < f.cursor_created_at) - OR ( - f.cursor_id IS NOT NULL - AND (b.created_at, b.id) < (f.cursor_created_at, f.cursor_id) - ) - ) - AND ( - f.search_term IS NULL - OR ( - f.resolved_template_id IS NOT NULL - AND f.candidate_build_id IS NOT NULL - AND (a.env_id = f.resolved_template_id OR b.id = f.candidate_build_id) - ) - OR ( - f.resolved_template_id IS NOT NULL - AND f.candidate_build_id IS NULL - AND a.env_id = f.resolved_template_id - ) - OR ( - f.resolved_template_id IS NULL - AND f.candidate_build_id IS NOT NULL - AND b.id = f.candidate_build_id - ) - ) - ORDER BY - b.created_at DESC, - b.id DESC, - a.created_at DESC NULLS LAST, - a.id DESC - LIMIT (SELECT requested_limit + 1 FROM params) -), -page_data AS ( - SELECT - p.id, - b.status, - b.reason::jsonb AS reason, - p.created_at, - b.finished_at, - p.env_id AS template_id - FROM page_ids p - JOIN public.env_builds b - ON b.id = p.id -) -SELECT - d.id, - d.status, - d.reason, - d.created_at, - CASE - WHEN d.finished_at IS NULL THEN NULL::timestamptz - ELSE d.finished_at - END AS finished_at, - d.template_id, - CASE - WHEN ea.alias IS NULL THEN NULL::text - ELSE ea.alias - END AS template_alias -FROM page_data d -LEFT JOIN LATERAL ( - SELECT x.alias - FROM public.env_aliases x - WHERE x.env_id = d.template_id - ORDER BY x.id ASC - LIMIT 1 -) ea ON TRUE -ORDER BY d.created_at DESC, d.id DESC; -$function$; - -REVOKE ALL ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) FROM PUBLIC; -GRANT EXECUTE ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) TO service_role; - -CREATE OR REPLACE FUNCTION public.list_team_running_build_statuses_rpc( - p_team_id uuid, - p_build_ids uuid[] -) -RETURNS TABLE ( - id uuid, - status text, - reason jsonb, - finished_at timestamptz -) -LANGUAGE sql -STABLE -SECURITY INVOKER -SET search_path = pg_catalog, public -AS $function$ -WITH requested_builds AS ( - SELECT DISTINCT requested.build_id - FROM UNNEST(COALESCE(p_build_ids, ARRAY[]::uuid[])) AS requested(build_id) -), -authorized_builds AS ( - SELECT DISTINCT ON (a.build_id) - a.build_id - FROM requested_builds r - JOIN public.env_build_assignments a - ON a.build_id = r.build_id - JOIN public.envs e - ON e.id = a.env_id - WHERE e.team_id = p_team_id - ORDER BY - a.build_id ASC, - a.created_at DESC NULLS LAST, - a.id DESC -) -SELECT - b.id, - b.status, - b.reason::jsonb AS reason, - b.finished_at -FROM authorized_builds ab -JOIN public.env_builds b - ON b.id = ab.build_id; -$function$; - -REVOKE ALL ON FUNCTION public.list_team_running_build_statuses_rpc(uuid, uuid[]) FROM PUBLIC; -GRANT EXECUTE ON FUNCTION public.list_team_running_build_statuses_rpc(uuid, uuid[]) TO service_role; - -COMMIT; diff --git a/migrations/20260217145145.sql b/migrations/20260217145145.sql deleted file mode 100644 index 8f87f0c9d..000000000 --- a/migrations/20260217145145.sql +++ /dev/null @@ -1,107 +0,0 @@ --- Timestamp: 20260217145145 - -BEGIN; - -CREATE OR REPLACE FUNCTION public.list_team_builds_rpc( - p_team_id uuid, - p_statuses text[] DEFAULT ARRAY['waiting', 'building', 'uploaded', 'failed']::text[], - p_limit integer DEFAULT 50, - p_cursor_created_at timestamptz DEFAULT NULL, - p_cursor_id uuid DEFAULT NULL, - p_build_id_or_template text DEFAULT NULL -) -RETURNS TABLE ( - id uuid, - status text, - reason jsonb, - created_at timestamptz, - finished_at timestamptz, - template_id text, - template_alias text -) -LANGUAGE sql -STABLE -SECURITY INVOKER -SET search_path = pg_catalog, public -AS $function$ -WITH params AS ( - SELECT - p_team_id AS team_id, - CASE - WHEN p_statuses IS NULL OR CARDINALITY(p_statuses) = 0 - THEN ARRAY['waiting', 'building', 'uploaded', 'failed']::text[] - ELSE p_statuses - END AS statuses, - GREATEST(1, LEAST(COALESCE(p_limit, 50), 100)) AS requested_limit, - COALESCE(p_cursor_created_at, 'infinity'::timestamptz) AS cursor_created_at, - CASE - WHEN p_cursor_created_at IS NULL - THEN 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid - WHEN p_cursor_id IS NULL - THEN '00000000-0000-0000-0000-000000000000'::uuid - ELSE p_cursor_id - END AS cursor_id, - NULLIF(BTRIM(p_build_id_or_template), '') AS search_term -), -resolved AS ( - SELECT - p.*, - CASE - WHEN p.search_term ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' - THEN p.search_term::uuid - ELSE NULL - END AS candidate_build_id, - COALESCE( - ( - SELECT e.id - FROM public.envs e - WHERE e.team_id = p.team_id - AND e.id = p.search_term - LIMIT 1 - ), - ( - SELECT ea.env_id - FROM public.env_aliases ea - JOIN public.envs e - ON e.id = ea.env_id - WHERE e.team_id = p.team_id - AND ea.alias = p.search_term - ORDER BY ea.alias ASC - LIMIT 1 - ) - ) AS resolved_template_id - FROM params p -) -SELECT - b.id, - b.status, - b.reason::jsonb AS reason, - b.created_at, - b.finished_at, - b.env_id AS template_id, - ea.alias AS template_alias -FROM public.env_builds b -JOIN resolved r - ON r.team_id = b.team_id -LEFT JOIN LATERAL ( - SELECT x.alias - FROM public.env_aliases x - WHERE x.env_id = b.env_id - ORDER BY x.alias ASC - LIMIT 1 -) ea ON TRUE -WHERE (b.created_at, b.id) < (r.cursor_created_at, r.cursor_id) - AND ( - r.search_term IS NULL - OR (r.candidate_build_id IS NOT NULL AND b.id = r.candidate_build_id) - OR (r.resolved_template_id IS NOT NULL AND b.env_id = r.resolved_template_id) - ) - AND b.status = ANY (r.statuses) -ORDER BY b.created_at DESC, b.id DESC -LIMIT (SELECT requested_limit + 1 FROM params) -$function$; - -REVOKE ALL ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) FROM PUBLIC; -GRANT EXECUTE ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) TO service_role; - -COMMIT; From 2fc7de4f97fd55e711c34ccf88ce6b927d9f6c83 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Tue, 17 Mar 2026 13:10:13 -0700 Subject: [PATCH 02/37] wip: dashboard api instead of supabase admin for public db access --- spec/openapi.dashboard-api.yaml | 403 ++++++ src/app/(auth)/auth/cli/page.tsx | 32 +- src/app/api/health/route.ts | 38 +- src/app/api/teams/user/route.ts | 38 +- .../inspect/sandbox/[sandboxId]/route.ts | 20 +- src/app/dashboard/[teamIdOrSlug]/layout.tsx | 58 +- .../dashboard/[teamIdOrSlug]/team-gate.tsx | 47 + src/app/dashboard/account/route.ts | 14 +- src/app/dashboard/route.ts | 14 +- src/app/sbx/new/route.ts | 12 +- src/configs/cache.ts | 6 - src/lib/clients/action.ts | 22 +- src/server/api/procedures.ts | 23 +- .../api/repositories/support.repository.ts | 40 +- src/server/api/routers/billing.ts | 30 +- src/server/api/routers/index.ts | 2 + src/server/api/routers/support.ts | 7 +- src/server/api/routers/teams.ts | 63 + src/server/api/routers/templates.ts | 214 +-- .../auth/check-user-team-auth-cached.ts | 48 - src/server/auth/get-default-team.ts | 55 - src/server/team/get-team-id-from-segment.ts | 52 +- src/server/team/get-team-limits-memo.ts | 65 - src/server/team/get-team-limits.ts | 25 +- src/server/team/get-team-members.ts | 60 +- src/server/team/get-team-memo.ts | 4 - src/server/team/get-team-pure.ts | 41 - src/server/team/get-team.ts | 59 +- src/server/team/get-user-teams-memo.ts | 10 - src/server/team/get-user-teams.ts | 100 -- src/server/team/resolve-user-team.ts | 89 +- src/server/team/team-actions.ts | 142 +- src/server/team/types.ts | 8 +- src/types/dashboard-api.types.ts | 1192 +++++++++++------ 34 files changed, 1710 insertions(+), 1323 deletions(-) create mode 100644 src/app/dashboard/[teamIdOrSlug]/team-gate.tsx create mode 100644 src/server/api/routers/teams.ts delete mode 100644 src/server/auth/check-user-team-auth-cached.ts delete mode 100644 src/server/auth/get-default-team.ts delete mode 100644 src/server/team/get-team-limits-memo.ts delete mode 100644 src/server/team/get-team-memo.ts delete mode 100644 src/server/team/get-team-pure.ts delete mode 100644 src/server/team/get-user-teams-memo.ts delete mode 100644 src/server/team/get-user-teams.ts diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml index acf6ac915..cac2b24e5 100644 --- a/spec/openapi.dashboard-api.yaml +++ b/spec/openapi.dashboard-api.yaml @@ -81,6 +81,29 @@ components: format: uuid maxItems: 100 uniqueItems: true + teamId: + name: teamId + in: path + required: true + description: Identifier of the team. + schema: + type: string + format: uuid + userId: + name: userId + in: path + required: true + description: Identifier of the user. + schema: + type: string + format: uuid + teamSlug: + name: slug + in: query + required: true + description: Team slug to resolve. + schema: + type: string responses: "400": @@ -320,9 +343,223 @@ components: type: string description: Human-readable health check result. + UserTeamLimits: + type: object + required: + - maxLengthHours + - concurrentSandboxes + - concurrentTemplateBuilds + - maxVcpu + - maxRamMb + - diskMb + properties: + maxLengthHours: + type: integer + format: int64 + concurrentSandboxes: + type: integer + format: int32 + concurrentTemplateBuilds: + type: integer + format: int32 + maxVcpu: + type: integer + format: int32 + maxRamMb: + type: integer + format: int32 + diskMb: + type: integer + format: int32 + + UserTeam: + type: object + required: + - id + - name + - slug + - tier + - email + - isDefault + - limits + properties: + id: + type: string + format: uuid + name: + type: string + slug: + type: string + tier: + type: string + email: + type: string + isDefault: + type: boolean + limits: + $ref: "#/components/schemas/UserTeamLimits" + + UserTeamsResponse: + type: object + required: + - teams + properties: + teams: + type: array + items: + $ref: "#/components/schemas/UserTeam" + + TeamMember: + type: object + required: + - id + - email + - isDefault + - createdAt + properties: + id: + type: string + format: uuid + email: + type: string + isDefault: + type: boolean + addedBy: + type: string + format: uuid + nullable: true + createdAt: + type: string + format: date-time + nullable: true + + TeamMembersResponse: + type: object + required: + - members + properties: + members: + type: array + items: + $ref: "#/components/schemas/TeamMember" + + UpdateTeamRequest: + type: object + required: + - name + properties: + name: + type: string + minLength: 1 + maxLength: 255 + + UpdateTeamResponse: + type: object + required: + - id + - name + properties: + id: + type: string + format: uuid + name: + type: string + + AddTeamMemberRequest: + type: object + required: + - email + properties: + email: + type: string + format: email + + DefaultTemplateAlias: + type: object + required: + - alias + properties: + alias: + type: string + namespace: + type: string + nullable: true + + DefaultTemplate: + type: object + required: + - id + - aliases + - buildId + - ramMb + - vcpu + - totalDiskSizeMb + - createdAt + - public + - buildCount + - spawnCount + properties: + id: + type: string + aliases: + type: array + items: + $ref: "#/components/schemas/DefaultTemplateAlias" + buildId: + type: string + format: uuid + ramMb: + type: integer + format: int64 + vcpu: + type: integer + format: int64 + totalDiskSizeMb: + type: integer + format: int64 + nullable: true + envdVersion: + type: string + nullable: true + createdAt: + type: string + format: date-time + public: + type: boolean + buildCount: + type: integer + format: int32 + spawnCount: + type: integer + format: int64 + + DefaultTemplatesResponse: + type: object + required: + - templates + properties: + templates: + type: array + items: + $ref: "#/components/schemas/DefaultTemplate" + + TeamResolveResponse: + type: object + required: + - id + - slug + properties: + id: + type: string + format: uuid + slug: + type: string + tags: - name: builds - name: sandboxes + - name: teams + - name: templates paths: /health: @@ -439,3 +676,169 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" + + /teams: + get: + summary: List user teams + description: Returns all teams the authenticated user belongs to, with limits and default flag. + tags: [teams] + security: + - Supabase1TokenAuth: [] + responses: + "200": + description: Successfully returned user teams. + content: + application/json: + schema: + $ref: "#/components/schemas/UserTeamsResponse" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /teams/resolve: + get: + summary: Resolve team identity + description: Resolves a team slug or UUID to the team's identity, validating the user is a member. + tags: [teams] + security: + - Supabase1TokenAuth: [] + parameters: + - $ref: "#/components/parameters/teamSlug" + responses: + "200": + description: Successfully resolved team. + content: + application/json: + schema: + $ref: "#/components/schemas/TeamResolveResponse" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + + /teams/{teamId}: + patch: + summary: Update team + tags: [teams] + security: + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/teamId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateTeamRequest" + responses: + "200": + description: Successfully updated team. + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateTeamResponse" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + + /teams/{teamId}/members: + get: + summary: List team members + tags: [teams] + security: + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/teamId" + responses: + "200": + description: Successfully returned team members. + content: + application/json: + schema: + $ref: "#/components/schemas/TeamMembersResponse" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + post: + summary: Add team member + tags: [teams] + security: + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/teamId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AddTeamMemberRequest" + responses: + "201": + description: Successfully added team member. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + + /teams/{teamId}/members/{userId}: + delete: + summary: Remove team member + tags: [teams] + security: + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/teamId" + - $ref: "#/components/parameters/userId" + responses: + "204": + description: Successfully removed team member. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "500": + $ref: "#/components/responses/500" + + /templates/defaults: + get: + summary: List default templates + description: Returns the list of default templates with their latest build info and aliases. + tags: [templates] + security: + - Supabase1TokenAuth: [] + responses: + "200": + description: Successfully returned default templates. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultTemplatesResponse" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index d116b3c44..8b57dca86 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -2,12 +2,13 @@ import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react' import { redirect } from 'next/navigation' import { Suspense } from 'react' import { serializeError } from 'serialize-error' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { generateE2BUserAccessToken } from '@/lib/utils/server' -import { getDefaultTeamRelation } from '@/server/auth/get-default-team' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' // Types @@ -21,7 +22,6 @@ type CLISearchParams = Promise<{ async function handleCLIAuth( next: string, - userId: string, userEmail: string, supabaseAccessToken: string ) { @@ -29,20 +29,25 @@ async function handleCLIAuth( throw new Error('Invalid redirect URL') } - try { - const defaultTeam = await getDefaultTeamRelation(userId) - const e2bAccessToken = await generateE2BUserAccessToken(supabaseAccessToken) + const { data: teamsData, error: teamsError } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(supabaseAccessToken), + }) - const searchParams = new URLSearchParams({ - email: userEmail, - accessToken: e2bAccessToken.token, - defaultTeamId: defaultTeam.team_id, - }) + const defaultTeam = teamsData?.teams.find((t) => t.isDefault) - return redirect(`${next}?${searchParams.toString()}`) - } catch (err) { - throw err + if (teamsError || !defaultTeam) { + throw new Error('Failed to resolve default team') } + + const e2bAccessToken = await generateE2BUserAccessToken(supabaseAccessToken) + + const searchParams = new URLSearchParams({ + email: userEmail, + accessToken: e2bAccessToken.token, + defaultTeamId: defaultTeam.id, + }) + + return redirect(`${next}?${searchParams.toString()}`) } // UI Components @@ -133,7 +138,6 @@ export default async function CLIAuthPage({ return await handleCLIAuth( next, - user.id, user.email!, session.access_token ) diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 56f59379d..62c6e686a 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,20 +1,17 @@ import { NextResponse } from 'next/server' import { serializeError } from 'serialize-error' +import { api } from '@/lib/clients/api' import { kv } from '@/lib/clients/kv' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' - -// NOTE - using cdn caching for rate limiting on db calls export const maxDuration = 10 export async function GET() { const checks = { kv: false, - supabase: false, + dashboardApi: false, } - // check kv try { await kv.ping() checks.kv = true @@ -28,26 +25,30 @@ export async function GET() { ) } - // check supabase - const { data: _, error } = await supabaseAdmin - .from('teams') - .select('id') - .limit(1) - .single() - - if (!error) { - checks.supabase = true - } else { + try { + const { error } = await api.GET('/health', {}) + if (!error) { + checks.dashboardApi = true + } else { + l.error( + { + key: 'health_check:dashboard_api_error', + error, + }, + 'Dashboard API health check failed' + ) + } + } catch (error) { l.error( { - key: 'health_check:supabase_error', + key: 'health_check:dashboard_api_error', error: serializeError(error), }, - 'Supabase health check failed' + 'Dashboard API health check failed' ) } - const allHealthy = checks.kv && checks.supabase + const allHealthy = checks.kv && checks.dashboardApi return NextResponse.json( { @@ -57,7 +58,6 @@ export async function GET() { { status: allHealthy ? 200 : 503, headers: { - // vercel infra respects this to cache on cdn 'Cache-Control': 'public, max-age=30, must-revalidate', }, } diff --git a/src/app/api/teams/user/route.ts b/src/app/api/teams/user/route.ts index d2dbd6066..c49e61d8a 100644 --- a/src/app/api/teams/user/route.ts +++ b/src/app/api/teams/user/route.ts @@ -1,20 +1,44 @@ +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/lib/clients/api' import { createClient } from '@/lib/clients/supabase/server' -import getUserTeamsMemo from '@/server/team/get-user-teams-memo' +import { getSessionInsecure } from '@/server/auth/get-session' +import type { ClientTeam } from '@/types/dashboard.types' import type { UserTeamsResponse } from './types' export async function GET() { try { const supabase = await createClient() - const { - data: { user }, - error, - } = await supabase.auth.getUser() + const session = await getSessionInsecure(supabase) - if (error || !user) { + if (!session) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - const teams = await getUserTeamsMemo(user) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(session.access_token), + }) + + if (error || !data?.teams) { + return Response.json( + { error: 'Failed to fetch teams' }, + { status: 500 } + ) + } + + const teams: ClientTeam[] = data.teams.map((t) => ({ + id: t.id, + name: t.name, + slug: t.slug, + tier: t.tier, + email: t.email, + is_default: t.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + })) return Response.json({ teams } satisfies UserTeamsResponse) } catch (error) { diff --git a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts index 08d788488..47f79b721 100644 --- a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts +++ b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts @@ -4,9 +4,8 @@ import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { infra } from '@/lib/clients/api' +import { api, infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' import { createClient } from '@/lib/clients/supabase/server' import { SandboxIdSchema } from '@/lib/schemas/api' import { setTeamCookies } from '@/lib/utils/cookies' @@ -159,17 +158,16 @@ export async function GET( } const accessToken = sessionResponse.session.access_token - const { data: userTeamRows, error: teamQueryError } = await supabaseAdmin - .from('users_teams') - .select('teams!inner(id, slug)') - .eq('user_id', userId) + const { data: teamsData, error: teamsError } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(accessToken), + }) - if (teamQueryError || !userTeamRows || userTeamRows.length === 0) { + if (teamsError || !teamsData?.teams || teamsData.teams.length === 0) { l.warn({ key: 'inspect_sandbox:teams_fetch_error', user_id: userId, sandbox_id: sandboxId, - error: teamQueryError, + error: teamsError, }) return redirectToDashboardWithWarning( @@ -182,9 +180,9 @@ export async function GET( ) } - const userTeams: UserTeam[] = userTeamRows.map((row) => ({ - id: row.teams.id, - slug: row.teams.slug, + const userTeams: UserTeam[] = teamsData.teams.map((team) => ({ + id: team.id, + slug: team.slug, })) const cookieStore = await cookies() diff --git a/src/app/dashboard/[teamIdOrSlug]/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/layout.tsx index 624d2444a..68f8b001f 100644 --- a/src/app/dashboard/[teamIdOrSlug]/layout.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/layout.tsx @@ -1,17 +1,15 @@ import { cookies } from 'next/headers' -import { redirect, unauthorized } from 'next/navigation' +import { redirect } from 'next/navigation' import type { Metadata } from 'next/types' -import { serializeError } from 'serialize-error' +import { DashboardTeamGate } from '@/app/dashboard/[teamIdOrSlug]/team-gate' import { COOKIE_KEYS } from '@/configs/cookies' import { METADATA } from '@/configs/metadata' import { AUTH_URLS } from '@/configs/urls' -import { DashboardContextProvider } from '@/features/dashboard/context' import DashboardLayoutView from '@/features/dashboard/layouts/layout' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { l } from '@/lib/clients/logger/logger' import { getSessionInsecure } from '@/server/auth/get-session' import getUserByToken from '@/server/auth/get-user-by-token' -import { getTeam } from '@/server/team/get-team' +import { HydrateClient, prefetch, prefetchAsync, trpc } from '@/trpc/server' import { SidebarInset, SidebarProvider } from '@/ui/primitives/sidebar' export const metadata: Metadata = { @@ -46,40 +44,26 @@ export default async function DashboardLayout({ throw redirect(AUTH_URLS.SIGN_IN) } - const teamRes = await getTeam({ teamIdOrSlug }) - const team = teamRes?.data - - if (!team) { - l.warn( - { - key: 'dashboard_layout:team_not_resolved', - user_id: data.user.id, - error: serializeError(teamRes?.serverError), - context: { - teamIdOrSlug, - }, - }, - `dashboard_layout:team_not_resolved - team not resolved for user (${data.user.id}) when accessing team (${teamIdOrSlug}) in dashboard layout` - ) - throw unauthorized() - } + prefetch(trpc.teams.getCurrentTeam.queryOptions({ teamIdOrSlug })) return ( - - -
-
- - - - {children} - - + + + +
+
+ + + + {children} + + +
-
- - + + + ) } diff --git a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx new file mode 100644 index 000000000..9bb8d3e55 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx @@ -0,0 +1,47 @@ +'use client' + +import type { User } from '@supabase/supabase-js' +import { QueryErrorResetBoundary, useSuspenseQuery } from '@tanstack/react-query' +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { DashboardContextProvider } from '@/features/dashboard/context' +import { useTRPC } from '@/trpc/client' +import Unauthorized from '../unauthorized' + +interface DashboardTeamGateProps { + teamIdOrSlug: string + user: User + children: React.ReactNode +} + +function TeamContent({ + teamIdOrSlug, + user, + children, +}: DashboardTeamGateProps) { + const trpc = useTRPC() + + const { data: team } = useSuspenseQuery( + trpc.teams.getCurrentTeam.queryOptions({ teamIdOrSlug }) + ) + + return ( + + {children} + + ) +} + +export function DashboardTeamGate(props: DashboardTeamGateProps) { + return ( + + {({ reset }) => ( + }> + + + + + )} + + ) +} diff --git a/src/app/dashboard/account/route.ts b/src/app/dashboard/account/route.ts index 99b96ad44..423f2bcc8 100644 --- a/src/app/dashboard/account/route.ts +++ b/src/app/dashboard/account/route.ts @@ -3,6 +3,7 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' +import { getSessionInsecure } from '@/server/auth/get-session' import { resolveUserTeam } from '@/server/team/resolve-user-team' export async function GET(request: NextRequest) { @@ -13,11 +14,15 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(new URL(AUTH_URLS.SIGN_IN, request.url)) } - // Resolve team for the user - const team = await resolveUserTeam(data.user.id) + const session = await getSessionInsecure(supabase) + + if (!session) { + return NextResponse.redirect(new URL(AUTH_URLS.SIGN_IN, request.url)) + } + + const team = await resolveUserTeam(session.access_token) if (!team) { - // UNEXPECTED STATE - sign out and redirect to sign-in await supabase.auth.signOut() const signInUrl = new URL(AUTH_URLS.SIGN_IN, request.url) @@ -29,16 +34,13 @@ export async function GET(request: NextRequest) { ) } - // Set team cookies for persistence await setTeamCookies(team.id, team.slug) - // Build redirect URL with team const redirectPath = PROTECTED_URLS.RESOLVED_ACCOUNT_SETTINGS( team.slug || team.id ) const redirectUrl = new URL(redirectPath, request.url) - // Preserve query parameters (e.g., reauth=1) request.nextUrl.searchParams.forEach((value, key) => { redirectUrl.searchParams.set(key, value) }) diff --git a/src/app/dashboard/route.ts b/src/app/dashboard/route.ts index c0bc9617a..61e46a557 100644 --- a/src/app/dashboard/route.ts +++ b/src/app/dashboard/route.ts @@ -3,6 +3,7 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' +import { getSessionInsecure } from '@/server/auth/get-session' import { resolveUserTeam } from '@/server/team/resolve-user-team' export const TAB_URL_MAP: Record string> = { @@ -19,7 +20,6 @@ export const TAB_URL_MAP: Record string> = { account: (_) => PROTECTED_URLS.ACCOUNT_SETTINGS, personal: (_) => PROTECTED_URLS.ACCOUNT_SETTINGS, - // back compatibility budget: (teamId) => PROTECTED_URLS.LIMITS(teamId), } @@ -35,10 +35,15 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(new URL('/sign-in', request.url)) } - const team = await resolveUserTeam(data.user.id) + const session = await getSessionInsecure(supabase) + + if (!session) { + return NextResponse.redirect(new URL('/sign-in', request.url)) + } + + const team = await resolveUserTeam(session.access_token) if (!team) { - // UNEXPECTED STATE - sign out and redirect to sign-in await supabase.auth.signOut() const signInUrl = new URL(AUTH_URLS.SIGN_IN, request.url) @@ -50,10 +55,8 @@ export async function GET(request: NextRequest) { ) } - // Set team cookies for persistence await setTeamCookies(team.id, team.slug) - // Determine redirect path based on tab parameter const urlGenerator = tab ? TAB_URL_MAP[tab] : null const redirectPath = urlGenerator ? urlGenerator(team.slug || team.id) @@ -61,7 +64,6 @@ export async function GET(request: NextRequest) { const redirectUrl = new URL(redirectPath, request.url) - // Forward ?support=true query param to auto-open the Contact Support dialog on the target page if (searchParams.get('support') === 'true') { redirectUrl.searchParams.set('support', 'true') } diff --git a/src/app/sbx/new/route.ts b/src/app/sbx/new/route.ts index 63e6cb3a5..05eed993a 100644 --- a/src/app/sbx/new/route.ts +++ b/src/app/sbx/new/route.ts @@ -3,9 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' -import { getDefaultTeam } from '@/server/auth/get-default-team' import { getSessionInsecure } from '@/server/auth/get-session' export const GET = async (req: NextRequest) => { @@ -35,7 +35,15 @@ export const GET = async (req: NextRequest) => { ) } - const defaultTeam = await getDefaultTeam(data.user.id) + const { data: teamsData, error: teamsError } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(session.access_token), + }) + + const defaultTeam = teamsData?.teams.find((t) => t.isDefault) + + if (teamsError || !defaultTeam) { + return NextResponse.redirect(new URL(req.url).origin) + } const sbx = await Sandbox.create('base', { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, diff --git a/src/configs/cache.ts b/src/configs/cache.ts index ab0042cca..35694d9c6 100644 --- a/src/configs/cache.ts +++ b/src/configs/cache.ts @@ -1,13 +1,7 @@ -/** - * Centralized cache tag configuration for Next.js "use cache" directive. - */ export const CACHE_TAGS = { - USER_TEAM_AUTHORIZATION: (userId: string, teamId: string) => - `user-team-authorization-${userId}-${teamId}`, USER_TEAMS: (userId: string) => `user-teams-${userId}`, TEAM_ID_FROM_SEGMENT: (segment: string) => `team-id-from-segment-${segment}`, - TEAM_TIER_LIMITS: (teamId: string) => `team-tier-limits-${teamId}`, TEAM_TEMPLATES: (teamId: string) => `team-templates-${teamId}`, TEAM_SANDBOXES_LIST: (teamId: string) => `team-sandboxes-list-${teamId}`, TEAM_USAGE: (teamId: string) => `team-usage-${teamId}`, diff --git a/src/lib/clients/action.ts b/src/lib/clients/action.ts index 5d8269db9..5723a7a3b 100644 --- a/src/lib/clients/action.ts +++ b/src/lib/clients/action.ts @@ -4,7 +4,6 @@ import { unauthorized } from 'next/navigation' import { createMiddleware, createSafeActionClient } from 'next-safe-action' import { serializeError } from 'serialize-error' import { z } from 'zod' -import checkUserTeamAuthCached from '@/server/auth/check-user-team-auth-cached' import { getSessionInsecure } from '@/server/auth/get-session' import getUserByToken from '@/server/auth/get-user-by-token' import { getTeamIdFromSegment } from '@/server/team/get-team-id-from-segment' @@ -217,7 +216,10 @@ export const withTeamIdResolution = createMiddleware<{ ) } - const teamId = await getTeamIdFromSegment(clientInput.teamIdOrSlug as string) + const teamId = await getTeamIdFromSegment( + clientInput.teamIdOrSlug as string, + ctx.session.access_token + ) if (!teamId) { l.warn( @@ -233,22 +235,6 @@ export const withTeamIdResolution = createMiddleware<{ throw unauthorized() } - const isAuthorized = await checkUserTeamAuthCached(ctx.user.id, teamId) - - if (!isAuthorized) { - l.warn( - { - key: 'with_team_id_resolution:user_not_authorized', - context: { - teamIdOrSlug: clientInput.teamIdOrSlug, - }, - }, - `with_team_id_resolution:user_not_authorized - user not authorized to access team: ${clientInput.teamIdOrSlug}` - ) - - throw unauthorized() - } - return next({ ctx: { teamId }, }) diff --git a/src/server/api/procedures.ts b/src/server/api/procedures.ts index e95e777b9..533aed02d 100644 --- a/src/server/api/procedures.ts +++ b/src/server/api/procedures.ts @@ -2,7 +2,6 @@ import { context, SpanStatusCode, trace } from '@opentelemetry/api' import z from 'zod' import { getTracer } from '@/lib/clients/tracer' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import checkUserTeamAuthCached from '../auth/check-user-team-auth-cached' import { getTeamIdFromSegment } from '../team/get-team-id-from-segment' import { forbiddenTeamAccessError } from './errors' import { t } from './init' @@ -76,7 +75,10 @@ export const protectedTeamProcedure = t.procedure const teamId = await context.with( trace.setSpan(context.active(), span), async () => { - return await getTeamIdFromSegment(input.teamIdOrSlug) + return await getTeamIdFromSegment( + input.teamIdOrSlug, + ctx.session.access_token + ) } ) @@ -86,23 +88,6 @@ export const protectedTeamProcedure = t.procedure message: `teamId not found for teamIdOrSlug (${input.teamIdOrSlug})`, }) - // the actual error should be 400, but we want to prevent leaking information to bad actors - throw forbiddenTeamAccessError() - } - - const isAuthorized = await context.with( - trace.setSpan(context.active(), span), - async () => { - return await checkUserTeamAuthCached(ctx.user.id, teamId) - } - ) - - if (!isAuthorized) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: `user (${ctx.user.id}) not authorized to access team (${teamId})`, - }) - throw forbiddenTeamAccessError() } diff --git a/src/server/api/repositories/support.repository.ts b/src/server/api/repositories/support.repository.ts index 1b8189c3c..8dba3c9f8 100644 --- a/src/server/api/repositories/support.repository.ts +++ b/src/server/api/repositories/support.repository.ts @@ -2,8 +2,9 @@ import 'server-only' import { AttachmentType, PlainClient } from '@team-plain/typescript-sdk' import { TRPCError } from '@trpc/server' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB per file const MAX_FILES = 5 @@ -142,21 +143,38 @@ async function uploadAttachmentToPlain( return attachment.id } -export async function getTeamSupportData(teamId: string) { - const { data: team, error: teamError } = await supabaseAdmin - .from('teams') - .select('name, email, tier') - .eq('id', teamId) - .single() +export async function getTeamSupportData( + teamId: string, + accessToken: string +) { + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(accessToken), + }) - if (teamError || !team) { + if (error) { l.error( { key: 'repositories:support:fetch_team_error', - error: teamError, + error, + team_id: teamId, + }, + `failed to fetch team data: ${error.message}` + ) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to load team information', + }) + } + + const team = data?.teams?.find((t) => t.id === teamId) + + if (!team) { + l.error( + { + key: 'repositories:support:fetch_team_not_found', team_id: teamId, }, - `failed to fetch team data: ${teamError?.message}` + `team not found in user teams` ) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', @@ -164,7 +182,7 @@ export async function getTeamSupportData(teamId: string) { }) } - return team + return { name: team.name, email: team.email, tier: team.tier } } export async function createSupportThread(input: { diff --git a/src/server/api/routers/billing.ts b/src/server/api/routers/billing.ts index 49bccddf7..c7772e658 100644 --- a/src/server/api/routers/billing.ts +++ b/src/server/api/routers/billing.ts @@ -6,7 +6,7 @@ import { ADDON_500_SANDBOXES_ID, ADDON_PURCHASE_ACTION_ERRORS, } from '@/features/dashboard/billing/constants' -import getTeamLimitsMemo from '@/server/team/get-team-limits-memo' +import { api } from '@/lib/clients/api' import type { AddOnOrderConfirmResponse, AddOnOrderCreateResponse, @@ -216,7 +216,33 @@ export const billingRouter = createTRPCRouter({ }), getTeamLimits: protectedTeamProcedure.query(async ({ ctx }) => { - return await getTeamLimitsMemo(ctx.teamId, ctx.user.id) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(ctx.session.access_token), + }) + + if (error || !data?.teams) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to fetch team limits', + }) + } + + const team = data.teams.find((t) => t.id === ctx.teamId) + + if (!team) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Team not found', + }) + } + + return { + concurrentInstances: team.limits.concurrentSandboxes, + diskMb: team.limits.diskMb, + maxLengthHours: team.limits.maxLengthHours, + maxRamMb: team.limits.maxRamMb, + maxVcpu: team.limits.maxVcpu, + } }), setLimit: protectedTeamProcedure diff --git a/src/server/api/routers/index.ts b/src/server/api/routers/index.ts index 27b9704d8..c8f492117 100644 --- a/src/server/api/routers/index.ts +++ b/src/server/api/routers/index.ts @@ -4,6 +4,7 @@ import { buildsRouter } from './builds' import { sandboxRouter } from './sandbox' import { sandboxesRouter } from './sandboxes' import { supportRouter } from './support' +import { teamsRouter } from './teams' import { templatesRouter } from './templates' export const trpcAppRouter = createTRPCRouter({ @@ -13,6 +14,7 @@ export const trpcAppRouter = createTRPCRouter({ builds: buildsRouter, billing: billingRouter, support: supportRouter, + teams: teamsRouter, }) export type TRPCAppRouter = typeof trpcAppRouter diff --git a/src/server/api/routers/support.ts b/src/server/api/routers/support.ts index bee9c215d..71ba2a5c4 100644 --- a/src/server/api/routers/support.ts +++ b/src/server/api/routers/support.ts @@ -21,7 +21,7 @@ export const supportRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { - const { teamId, user } = ctx + const { teamId, session, user } = ctx const email = user.email if (!email) { @@ -36,7 +36,10 @@ export const supportRouter = createTRPCRouter({ }) } - const team = await supportRepo.getTeamSupportData(teamId) + const team = await supportRepo.getTeamSupportData( + teamId, + session.access_token + ) return supportRepo.createSupportThread({ description: input.description, diff --git a/src/server/api/routers/teams.ts b/src/server/api/routers/teams.ts new file mode 100644 index 000000000..9dfc7e7b3 --- /dev/null +++ b/src/server/api/routers/teams.ts @@ -0,0 +1,63 @@ +import { TRPCError } from '@trpc/server' +import z from 'zod' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/lib/clients/api' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import type { ClientTeam } from '@/types/dashboard.types' +import { protectedProcedure } from '../procedures' + +function mapApiTeamToClientTeam( + apiTeam: { + id: string + name: string + slug: string + tier: string + email: string + isDefault: boolean + }, +): ClientTeam { + return { + id: apiTeam.id, + name: apiTeam.name, + slug: apiTeam.slug, + tier: apiTeam.tier, + email: apiTeam.email, + is_default: apiTeam.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + } +} + +export const teamsRouter = { + getCurrentTeam: protectedProcedure + .input(z.object({ teamIdOrSlug: TeamIdOrSlugSchema })) + .query(async ({ ctx, input }) => { + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(ctx.session.access_token), + }) + + if (error || !data?.teams) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to fetch teams', + }) + } + + const apiTeam = data.teams.find( + (t) => t.slug === input.teamIdOrSlug || t.id === input.teamIdOrSlug + ) + + if (!apiTeam) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Team not found or access denied', + }) + } + + return mapApiTeamToClientTeam(apiTeam) + }), +} diff --git a/src/server/api/routers/templates.ts b/src/server/api/routers/templates.ts index 8ed03f0ed..79d393e34 100644 --- a/src/server/api/routers/templates.ts +++ b/src/server/api/routers/templates.ts @@ -1,5 +1,4 @@ import { TRPCError } from '@trpc/server' -import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' @@ -8,9 +7,8 @@ import { MOCK_DEFAULT_TEMPLATES_DATA, MOCK_TEMPLATES_DATA, } from '@/configs/mock-data' -import { infra } from '@/lib/clients/api' +import { api, infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' import type { DefaultTemplate } from '@/types/api.types' import { apiError } from '../errors' import { createTRPCRouter } from '../init' @@ -64,8 +62,8 @@ export const templatesRouter = createTRPCRouter({ } }), - getDefaultTemplatesCached: protectedProcedure.query(async () => { - return getDefaultTemplatesCached() + getDefaultTemplatesCached: protectedProcedure.query(async ({ ctx }) => { + return getDefaultTemplatesCached(ctx.session.access_token) }), // MUTATIONS @@ -190,11 +188,7 @@ export const templatesRouter = createTRPCRouter({ }), }) -async function getDefaultTemplatesCached() { - 'use cache: remote' - cacheTag(CACHE_TAGS.DEFAULT_TEMPLATES) - cacheLife('hours') - +async function getDefaultTemplatesCached(accessToken: string) { if (USE_MOCK_DATA) { await new Promise((resolve) => setTimeout(resolve, 500)) return { @@ -202,176 +196,42 @@ async function getDefaultTemplatesCached() { } } - const { data: defaultEnvs, error: defaultEnvsError } = await supabaseAdmin - .from('env_defaults') - .select('*') - - if (defaultEnvsError) { - throw defaultEnvsError - } - - if (!defaultEnvs || defaultEnvs.length === 0) { - return { - templates: [], - } - } - - const envIds = defaultEnvs.map((env) => env.env_id) - - const { data: envs, error: envsError } = await supabaseAdmin - .from('envs') - .select( - ` - id, - created_at, - updated_at, - public, - build_count, - spawn_count, - last_spawned_at, - created_by - ` - ) - .in('id', envIds) + const { data, error } = await api.GET('/templates/defaults', { + headers: SUPABASE_AUTH_HEADERS(accessToken), + next: { tags: [CACHE_TAGS.DEFAULT_TEMPLATES] }, + }) - if (envsError) { - throw envsError + if (error) { + throw new Error(error.message) } - const templates: DefaultTemplate[] = [] - - for (const env of envs) { - const { data: latestAssignment, error: latestAssignmentError } = - await supabaseAdmin - .from('env_build_assignments') - .select('build_id, env_id, env_builds!inner(status)') - .eq('env_id', env.id) - .eq('env_builds.status', 'uploaded') - .order('created_at', { ascending: false, nullsFirst: false }) - .order('id', { ascending: false }) - .limit(1) - .maybeSingle() - - if (latestAssignmentError) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_build_assignments_supabase_error', - error: latestAssignmentError, - template_id: env.id, - }, - `Failed to query latest uploaded template build assignment: ${latestAssignmentError.message || 'Unknown error'}` - ) - continue - } - - if (!latestAssignment) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_build_assignments_missing_latest', - template_id: env.id, - }, - `Failed to get latest uploaded template build assignment: assignment not found` - ) - continue - } - const latestBuildId = latestAssignment.build_id - - const { data: latestBuild, error: buildError } = await supabaseAdmin - .from('env_builds') - .select('id, env_id, ram_mb, vcpu, total_disk_size_mb, envd_version') - .eq('id', latestBuildId) - .single() - - if (buildError) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_builds_supabase_error', - error: buildError, - template_id: env.id, - build_id: latestBuildId, - }, - `Failed to get template builds: ${buildError.message || 'Unknown error'}` - ) - continue - } - - if (latestAssignment.env_id !== env.id) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_build_assignment_mismatch', - template_id: env.id, - build_id: latestBuildId, - assignment_env_id: latestAssignment.env_id, - }, - 'Build assignment env_id mismatch with template env_id' - ) - continue - } - - const { data: aliases, error: aliasesError } = await supabaseAdmin - .from('env_aliases') - .select('alias, namespace') - .eq('env_id', env.id) - - if (aliasesError) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_aliases_supabase_error', - error: aliasesError, - template_id: env.id, - }, - `Failed to get template aliases: ${aliasesError.message || 'Unknown error'}` - ) - continue - } - - const diskSizeMB = latestBuild.total_disk_size_mb - const envdVersion = latestBuild.envd_version - - if ( - diskSizeMB == null || - diskSizeMB <= 0 || - envdVersion == null || - envdVersion.trim().length === 0 - ) { - l.error( - { - key: 'trpc:templates:get_default_templates:env_builds_missing_values', - template_id: env.id, - }, - `Template build missing required values: total_disk_size_mb or envd_version` - ) - continue - } - - templates.push({ - templateID: env.id, - buildID: latestBuild.id, - cpuCount: latestBuild.vcpu, - memoryMB: latestBuild.ram_mb, - diskSizeMB, - envdVersion, - public: env.public, - aliases: aliases.map((a) => a.alias), - names: aliases.map((a) => { - if (a.namespace && a.namespace.length > 0) { - return `${a.namespace}/${a.alias}` - } - return a.alias - }), - createdAt: env.created_at, - updatedAt: env.updated_at, - createdBy: null, - lastSpawnedAt: env.last_spawned_at ?? env.created_at, - spawnCount: env.spawn_count, - buildCount: env.build_count, - isDefault: true, - defaultDescription: - defaultEnvs.find((e) => e.env_id === env.id)?.description ?? undefined, - }) + if (!data?.templates || data.templates.length === 0) { + return { templates: [] as DefaultTemplate[] } } - return { - templates: templates, - } + const templates: DefaultTemplate[] = data.templates.map((t) => ({ + templateID: t.id, + buildID: t.buildId, + cpuCount: t.vcpu, + memoryMB: t.ramMb, + diskSizeMB: t.totalDiskSizeMb ?? 0, + envdVersion: t.envdVersion ?? '', + public: t.public, + aliases: t.aliases.map((a) => a.alias), + names: t.aliases.map((a) => { + if (a.namespace && a.namespace.length > 0) { + return `${a.namespace}/${a.alias}` + } + return a.alias + }), + createdAt: t.createdAt, + updatedAt: t.createdAt, + createdBy: null, + lastSpawnedAt: t.createdAt, + spawnCount: t.spawnCount, + buildCount: t.buildCount, + isDefault: true as const, + })) + + return { templates } } diff --git a/src/server/auth/check-user-team-auth-cached.ts b/src/server/auth/check-user-team-auth-cached.ts deleted file mode 100644 index 9d088f5c5..000000000 --- a/src/server/auth/check-user-team-auth-cached.ts +++ /dev/null @@ -1,48 +0,0 @@ -import 'server-only' - -import { cacheTag } from 'next/cache' -import { serializeError } from 'serialize-error' -import { CACHE_TAGS } from '@/configs/cache' -import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' - -export async function checkUserTeamAuth(userId: string, teamId: string) { - const { data: userTeamsRelationData, error: userTeamsRelationError } = - await supabaseAdmin - .from('users_teams') - .select('*') - .eq('user_id', userId) - .eq('team_id', teamId) - - if (userTeamsRelationError) { - l.error( - { - key: 'check_user_team_authorization:failed_to_fetch_users_teams_relation', - error: serializeError(userTeamsRelationError), - context: { - userId, - teamId, - }, - }, - `Failed to fetch users_teams relation (user: ${userId}, team: ${teamId})` - ) - - return false - } - - return !!userTeamsRelationData.length -} - -/* - * This function checks if a user is authorized to access a team. - * If the user is not authorized, it returns false. - */ -export default async function checkUserTeamAuthCached( - userId: string, - teamId: string -) { - 'use cache' - cacheTag(CACHE_TAGS.USER_TEAM_AUTHORIZATION(userId, teamId)) - - return checkUserTeamAuth(userId, teamId) -} diff --git a/src/server/auth/get-default-team.ts b/src/server/auth/get-default-team.ts deleted file mode 100644 index f3820d68f..000000000 --- a/src/server/auth/get-default-team.ts +++ /dev/null @@ -1,55 +0,0 @@ -import 'server-cli-only' - -import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' - -export async function getDefaultTeamRelation(userId: string) { - const { data, error } = await supabaseAdmin - .from('users_teams') - .select('*') - .eq('user_id', userId) - .eq('is_default', true) - - if (error || data.length === 0) { - l.error({ - key: 'get_default_team_relation:error', - error: serializeError(error), - user_id: userId, - }) - - throw new Error('No default team found') - } - - return data[0]! -} - -export async function getDefaultTeam(userId: string) { - const { data, error } = await supabaseAdmin - .from('users_teams') - .select( - ` - team_id, - teams ( - id, - name, - slug - ) - ` - ) - .eq('user_id', userId) - .eq('is_default', true) - .single() - - if (error || !data) { - l.error({ - key: 'GET_DEFAULT_TEAM:ERROR', - message: error?.message, - error: serializeError(error), - user_id: userId, - }) - throw new Error('No default team found') - } - - return data.teams -} diff --git a/src/server/team/get-team-id-from-segment.ts b/src/server/team/get-team-id-from-segment.ts index c01a4d1f7..d266bd8f4 100644 --- a/src/server/team/get-team-id-from-segment.ts +++ b/src/server/team/get-team-id-from-segment.ts @@ -1,26 +1,21 @@ import 'server-only' -import { cacheLife } from 'next/dist/server/use-cache/cache-life' -import { cacheTag } from 'next/dist/server/use-cache/cache-tag' -import { serializeError } from 'serialize-error' import z from 'zod' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -export const getTeamIdFromSegment = async (segment: string) => { - 'use cache' - cacheLife('default') - cacheTag(CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)) - +export const getTeamIdFromSegment = async ( + segment: string, + accessToken: string +) => { if (!TeamIdOrSlugSchema.safeParse(segment).success) { l.warn( { key: 'get_team_id_from_segment:invalid_segment', - context: { - segment, - }, + context: { segment }, }, 'get_team_id_from_segment - invalid segment' ) @@ -29,37 +24,26 @@ export const getTeamIdFromSegment = async (segment: string) => { } if (z.uuid().safeParse(segment).success) { - // make sure this uuid is a valid teamId and is not it's slug - const { data } = await supabaseAdmin - .from('teams') - .select('id') - .not('slug', 'eq', segment) - .eq('id', segment) - - if (data?.length) { - return data[0]!.id - } + return segment } - const { data, error } = await supabaseAdmin - .from('teams') - .select('id') - .eq('slug', segment) + const { data, error } = await api.GET('/teams/resolve', { + params: { query: { slug: segment } }, + headers: SUPABASE_AUTH_HEADERS(accessToken), + next: { tags: [CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)] }, + }) - if (error || !data.length) { + if (error || !data) { l.warn( { - key: 'get_team_id_from_segment:failed_to_get_team_id', - error: serializeError(error), - context: { - segment, - }, + key: 'get_team_id_from_segment:resolve_failed', + context: { segment }, }, - 'get_team_id_from_segment - failed to get team id' + 'get_team_id_from_segment - failed to resolve' ) return null } - return data[0]!.id + return data.id } diff --git a/src/server/team/get-team-limits-memo.ts b/src/server/team/get-team-limits-memo.ts deleted file mode 100644 index 20d2ee6a8..000000000 --- a/src/server/team/get-team-limits-memo.ts +++ /dev/null @@ -1,65 +0,0 @@ -import 'server-cli-only' - -import { cache } from 'react' -import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import type { TeamLimits } from './get-team-limits' - -/** - * Internal function to fetch team limits from the database - */ -async function _getTeamLimits( - teamId: string, - userId: string -): Promise { - try { - const { data: teamData, error: teamError } = await supabaseAdmin - .from('team_limits') - .select('*') - .eq('id', teamId) - .single() - - if (teamError) { - l.error({ - key: 'get_team_limits_memo:team_query_error', - message: teamError.message, - error: serializeError(teamError), - team_id: teamId, - user_id: userId, - }) - return null - } - - if (!teamData) { - l.error({ - key: 'get_team_limits_memo:no_team_data', - message: 'No data found for team', - team_id: teamId, - user_id: userId, - }) - return null - } - - return { - concurrentInstances: teamData.concurrent_sandboxes || 0, - diskMb: teamData.disk_mb || 0, - maxLengthHours: teamData.max_length_hours || 0, - maxRamMb: teamData.max_ram_mb || 0, - maxVcpu: teamData.max_vcpu || 0, - } - } catch (error) { - l.error({ - key: 'get_team_limits_memo:unexpected_error', - message: 'Unexpected error fetching team limits', - error: serializeError(error), - team_id: teamId, - user_id: userId, - }) - return null - } -} - -const getTeamLimitsMemo = cache(_getTeamLimits) - -export default getTeamLimitsMemo diff --git a/src/server/team/get-team-limits.ts b/src/server/team/get-team-limits.ts index 1e70b6b91..c7b45a511 100644 --- a/src/server/team/get-team-limits.ts +++ b/src/server/team/get-team-limits.ts @@ -1,11 +1,12 @@ import 'server-only' import { z } from 'zod' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { USE_MOCK_DATA } from '@/configs/flags' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { api } from '@/lib/clients/api' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' -import getTeamLimitsMemo from './get-team-limits-memo' export interface TeamLimits { concurrentInstances: number @@ -32,17 +33,31 @@ export const getTeamLimits = authActionClient .metadata({ serverFunctionName: 'getTeamLimits' }) .use(withTeamIdResolution) .action(async ({ ctx }) => { - const { user, teamId } = ctx + const { teamId, session } = ctx if (USE_MOCK_DATA) { return MOCK_TIER_LIMITS } - const tierLimits = await getTeamLimitsMemo(teamId, user.id) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(session.access_token), + }) - if (!tierLimits) { + if (error || !data?.teams) { return returnServerError('Failed to fetch team limits') } - return tierLimits + const team = data.teams.find((t) => t.id === teamId) + + if (!team) { + return returnServerError('Team not found') + } + + return { + concurrentInstances: team.limits.concurrentSandboxes, + diskMb: team.limits.diskMb, + maxLengthHours: team.limits.maxLengthHours, + maxRamMb: team.limits.maxRamMb, + maxVcpu: team.limits.maxVcpu, + } satisfies TeamLimits }) diff --git a/src/server/team/get-team-members.ts b/src/server/team/get-team-members.ts index b0b7270db..98a4fa077 100644 --- a/src/server/team/get-team-members.ts +++ b/src/server/team/get-team-members.ts @@ -1,7 +1,8 @@ import 'server-only' -import type { User } from '@supabase/supabase-js' import { z } from 'zod' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/lib/clients/api' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' import { supabaseAdmin } from '@/lib/clients/supabase/admin' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' @@ -16,42 +17,43 @@ export const getTeamMembers = authActionClient .metadata({ serverFunctionName: 'getTeamMembers' }) .use(withTeamIdResolution) .action(async ({ ctx }) => { - const { teamId } = ctx + const { teamId, session } = ctx - const { data, error } = await supabaseAdmin - .from('users_teams') - .select('*') - .eq('team_id', teamId) + const { data, error } = await api.GET('/teams/{teamId}/members', { + params: { path: { teamId } }, + headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), + }) if (error) { - throw error + throw new Error(error.message) } - if (!data) { + if (!data?.members || data.members.length === 0) { return [] } - const userResponses = await Promise.all( - data.map( - async (userTeam) => - (await supabaseAdmin.auth.admin.getUserById(userTeam.user_id)).data - .user - ) + const enrichedMembers = await Promise.all( + data.members.map(async (member) => { + const { data: userData } = + await supabaseAdmin.auth.admin.getUserById(member.id) + + const user = userData.user + const info: TeamMemberInfo = { + id: member.id, + email: member.email, + name: user?.user_metadata?.name, + avatar_url: user?.user_metadata?.avatar_url, + } + + return { + info, + relation: { + added_by: member.addedBy ?? null, + is_default: member.isDefault, + }, + } + }) ) - return userResponses - .filter((user) => user !== null) - .map((user) => ({ - info: memberDTO(user), - relation: data.find((userTeam) => userTeam.user_id === user.id)!, - })) + return enrichedMembers }) - -function memberDTO(user: User): TeamMemberInfo { - return { - id: user.id, - email: user.email!, - name: user.user_metadata?.name, - avatar_url: user.user_metadata?.avatar_url, - } -} diff --git a/src/server/team/get-team-memo.ts b/src/server/team/get-team-memo.ts deleted file mode 100644 index f5a4c079c..000000000 --- a/src/server/team/get-team-memo.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { cache } from 'react' -import { getTeamPure } from './get-team-pure' - -export default cache(getTeamPure) diff --git a/src/server/team/get-team-pure.ts b/src/server/team/get-team-pure.ts deleted file mode 100644 index cb28d7859..000000000 --- a/src/server/team/get-team-pure.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'server-cli-only' - -import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import { returnServerError } from '@/lib/utils/action' -import type { ClientTeam } from '@/types/dashboard.types' - -export const getTeamPure = async (userId: string, teamId: string) => { - const { data: userTeamsRelationData, error: userTeamsRelationError } = - await supabaseAdmin - .from('users_teams') - .select('*') - .eq('user_id', userId) - .eq('team_id', teamId) - - if (userTeamsRelationError) { - throw userTeamsRelationError - } - - if (!userTeamsRelationData || userTeamsRelationData.length === 0) { - return returnServerError('User is not authorized to view this team') - } - - const relation = userTeamsRelationData[0]! - - const { data: team, error: teamError } = await supabaseAdmin - .from('teams') - .select('*') - .eq('id', teamId) - .single() - - if (teamError) { - throw teamError - } - - const ClientTeam: ClientTeam = { - ...team, - is_default: relation.is_default, - } - - return ClientTeam -} diff --git a/src/server/team/get-team.ts b/src/server/team/get-team.ts index a16e453bf..695d81a8f 100644 --- a/src/server/team/get-team.ts +++ b/src/server/team/get-team.ts @@ -1,11 +1,12 @@ import 'server-cli-only' import { z } from 'zod' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { api } from '@/lib/clients/api' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' -import getTeamMemo from './get-team-memo' -import getUserTeamsMemo from './get-user-teams-memo' +import type { ClientTeam } from '@/types/dashboard.types' const GetTeamSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, @@ -16,9 +17,36 @@ export const getTeam = authActionClient .metadata({ serverFunctionName: 'getTeam' }) .use(withTeamIdResolution) .action(async ({ ctx }) => { - const { teamId, user } = ctx + const { teamId, session } = ctx - const team = await getTeamMemo(user.id, teamId) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(session.access_token), + }) + + if (error || !data?.teams) { + return returnServerError('Failed to fetch team') + } + + const apiTeam = data.teams.find((t) => t.id === teamId) + + if (!apiTeam) { + return returnServerError('Team not found') + } + + const team: ClientTeam = { + id: apiTeam.id, + name: apiTeam.name, + slug: apiTeam.slug, + tier: apiTeam.tier, + email: apiTeam.email, + is_default: apiTeam.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + } return team }) @@ -26,13 +54,30 @@ export const getTeam = authActionClient export const getUserTeams = authActionClient .metadata({ serverFunctionName: 'getUserTeams' }) .action(async ({ ctx }) => { - const { user } = ctx + const { session } = ctx - const teams = await getUserTeamsMemo(user) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(session.access_token), + }) - if (!teams || teams.length === 0) { + if (error || !data?.teams || data.teams.length === 0) { return returnServerError('No teams found.') } + const teams: ClientTeam[] = data.teams.map((t) => ({ + id: t.id, + name: t.name, + slug: t.slug, + tier: t.tier, + email: t.email, + is_default: t.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + })) + return teams }) diff --git a/src/server/team/get-user-teams-memo.ts b/src/server/team/get-user-teams-memo.ts deleted file mode 100644 index b92ddb875..000000000 --- a/src/server/team/get-user-teams-memo.ts +++ /dev/null @@ -1,10 +0,0 @@ -import 'server-cli-only' - -import type { User } from '@supabase/supabase-js' -import { cache } from 'react' - -import { getUserTeams } from './get-user-teams' - -const getUserTeamsMemo = cache((user: User) => getUserTeams(user)) - -export default getUserTeamsMemo diff --git a/src/server/team/get-user-teams.ts b/src/server/team/get-user-teams.ts deleted file mode 100644 index 2aa93c51a..000000000 --- a/src/server/team/get-user-teams.ts +++ /dev/null @@ -1,100 +0,0 @@ -import 'server-cli-only' - -import type { User } from '@supabase/supabase-js' -import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import type { ClientTeam } from '@/types/dashboard.types' - -export async function getUserTeams(user: User): Promise { - const { data: usersTeamsData, error } = await supabaseAdmin - .from('users_teams') - .select('*, teams (*)') - .eq('user_id', user.id) - - if (error) { - throw error - } - - if (!usersTeamsData || usersTeamsData.length === 0) { - return [] - } - - const teamIds = usersTeamsData.map((userTeam) => userTeam.teams.id) - - try { - const { data: allConnectedDefaultTeamRelations, error: relationsError } = - await supabaseAdmin - .from('users_teams') - .select('team_id, user_id, is_default') - .in('team_id', teamIds) - .eq('is_default', true) - - if (relationsError) { - throw relationsError - } - - const defaultUserIds = new Set( - allConnectedDefaultTeamRelations?.map((r) => r.user_id) ?? [] - ) - - const { data: defaultTeamAuthUsers, error: authUsersError } = - await supabaseAdmin - .from('auth_users') - .select('id, email') - .in('id', Array.from(defaultUserIds)) - - if (authUsersError) { - l.error({ - key: 'get_usr_teams:supabase_error', - message: authUsersError.message, - error: serializeError(authUsersError), - user_id: user.id, - }) - } - - const userEmailMap = new Map( - defaultTeamAuthUsers?.map((u) => [u.id, u.email]) ?? [] - ) - - return usersTeamsData.map((userTeam) => { - const team = userTeam.teams - const defaultTeamRelation = allConnectedDefaultTeamRelations?.find( - (relation) => relation.team_id === team.id - ) - - let transformedDefaultName: string | undefined - - if ( - defaultTeamRelation && - team.name === userEmailMap.get(defaultTeamRelation.user_id) - ) { - const [username] = team.name.split('@') - if (username) { - transformedDefaultName = - username.charAt(0).toUpperCase() + username.slice(1) + "'s Team" - } - } - - return { - ...team, - is_default: userTeam.is_default, - transformed_default_name: transformedDefaultName, - } - }) - } catch (err) { - l.error({ - key: 'get_user_teams:unexpected_error', - error: serializeError(err), - user_id: user.id, - context: { - usersTeamsData, - }, - }) - - return usersTeamsData.map((userTeam) => ({ - ...userTeam.teams, - is_default: userTeam.is_default, - })) - } -} diff --git a/src/server/team/resolve-user-team.ts b/src/server/team/resolve-user-team.ts index edb5fcd03..baf0cc686 100644 --- a/src/server/team/resolve-user-team.ts +++ b/src/server/team/resolve-user-team.ts @@ -1,104 +1,51 @@ import 'server-only' import { cookies } from 'next/headers' -import { serializeError } from 'serialize-error' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import { checkUserTeamAuth } from '../auth/check-user-team-auth-cached' import type { ResolvedTeam } from './types' -/** - * Resolves team ID and slug for a user using this priority: - * 1. Cookie values (if exist and user is authorized) - * 2. Database default team - * 3. Database first team - * - * This function centralizes all team resolution logic used across route handlers. - * - * @param userId - The user ID to resolve team for - * @returns ResolvedTeam with team ID and slug, or null if no team found - */ export async function resolveUserTeam( - userId: string + accessToken: string ): Promise { const cookieStore = await cookies() - // Try to get team from cookies first const cookieTeamId = cookieStore.get(COOKIE_KEYS.SELECTED_TEAM_ID)?.value const cookieTeamSlug = cookieStore.get(COOKIE_KEYS.SELECTED_TEAM_SLUG)?.value - // If we have cookies, check if the user is authorized to access the team if (cookieTeamId && cookieTeamSlug) { - const isAuthorized = await checkUserTeamAuth(userId, cookieTeamId) - - if (isAuthorized) { - return { - id: cookieTeamId, - slug: cookieTeamSlug, - } - } + return { id: cookieTeamId, slug: cookieTeamSlug } } - // No valid cookies, query database for user's teams - const { data: teamsData, error } = await supabaseAdmin - .from('users_teams') - .select( - ` - team_id, - is_default, - team:teams( - id, - slug - ) - ` - ) - .eq('user_id', userId) + const { data, error } = await api.GET('/teams', { + headers: SUPABASE_AUTH_HEADERS(accessToken), + }) - if (error) { + if (error || !data?.teams) { l.error( { - key: 'resolve_user_team:db_error', - userId, - error: serializeError(error), + key: 'resolve_user_team:api_error', }, - 'Failed to query user teams' + 'Failed to fetch user teams' ) return null } - if (!teamsData || teamsData.length === 0) { + if (data.teams.length === 0) { return null } - // Try to get default team first - const defaultTeam = teamsData.find((t) => t.is_default) + const defaultTeam = data.teams.find((t) => t.isDefault) + const team = defaultTeam ?? data.teams[0] - if (defaultTeam?.team) { - return { - id: defaultTeam.team_id, - slug: defaultTeam.team.slug || defaultTeam.team_id, - } + if (!team) { + return null } - // Fallback to first team - const firstTeam = teamsData[0]! - - if (firstTeam?.team) { - return { - id: firstTeam.team_id, - slug: firstTeam.team.slug || firstTeam.team_id, - } + return { + id: team.id, + slug: team.slug, } - - l.error( - { - key: 'resolve_user_team:malformed_data', - userId, - teamsData, - }, - 'Teams data exists but malformed (no team relation)' - ) - - return null } diff --git a/src/server/team/team-actions.ts b/src/server/team/team-actions.ts index 677010ced..5dbd5a1e3 100644 --- a/src/server/team/team-actions.ts +++ b/src/server/team/team-actions.ts @@ -1,15 +1,15 @@ 'use server' import { fileTypeFromBuffer } from 'file-type' -import { revalidatePath, revalidateTag } from 'next/cache' +import { revalidatePath } from 'next/cache' import { after } from 'next/server' import { returnValidationErrors } from 'next-safe-action' import { serializeError } from 'serialize-error' import { z } from 'zod' import { zfd } from 'zod-form-data' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { CACHE_TAGS } from '@/configs/cache' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' import { supabaseAdmin } from '@/lib/clients/supabase/admin' @@ -21,21 +21,19 @@ import type { CreateTeamsResponse } from '@/types/billing.types' export const updateTeamNameAction = authActionClient .schema(UpdateTeamNameSchema) .metadata({ actionName: 'updateTeamName' }) - .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { name, teamIdOrSlug } = parsedInput - const { teamId } = ctx + const { teamId, session } = ctx - const { data, error } = await supabaseAdmin - .from('teams') - .update({ name }) - .eq('id', teamId) - .select() - .single() + const { data, error } = await api.PATCH('/teams/{teamId}', { + params: { path: { teamId } }, + headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), + body: { name }, + }) if (error) { - return returnServerError(`Failed to update team name: ${error.message}`) + return returnServerError('Failed to update team name') } revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') @@ -54,53 +52,20 @@ export const addTeamMemberAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { email, teamIdOrSlug } = parsedInput - const { teamId, user } = ctx + const { teamId, session } = ctx - const { data: existingUsers, error: userError } = await supabaseAdmin - .from('auth_users') - .select('*') - .eq('email', email) - - if (userError) { - return returnServerError(`Error finding user: ${userError.message}`) - } - - const existingUser = existingUsers?.[0] - - if (!existingUser || !existingUser.id) { - return returnServerError( - 'User with this email address does not exist. Please ask them to sign up first and try again.' - ) - } - - const { data: existingTeamMember } = await supabaseAdmin - .from('users_teams') - .select('*') - .eq('team_id', teamId) - .eq('user_id', existingUser.id) - .single() - - if (existingTeamMember) { - return returnServerError('User is already a member of this team') - } - - const { error: insertError } = await supabaseAdmin - .from('users_teams') - .insert({ - team_id: teamId, - user_id: existingUser.id, - added_by: user.id, - }) + const { error } = await api.POST('/teams/{teamId}/members', { + params: { path: { teamId } }, + headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), + body: { email }, + }) - if (insertError) { - return returnServerError( - `Failed to add team member: ${insertError.message}` - ) + if (error) { + const message = + (error as { message?: string }).message ?? 'Failed to add team member' + return returnServerError(message) } - revalidateTag(CACHE_TAGS.USER_TEAM_AUTHORIZATION(existingUser.id, teamId), { - expire: 0, - }) revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') }) @@ -115,52 +80,20 @@ export const removeTeamMemberAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { userId, teamIdOrSlug } = parsedInput - const { teamId, user } = ctx - - const { data: teamMemberData, error: teamMemberError } = await supabaseAdmin - .from('users_teams') - .select('*') - .eq('team_id', teamId) - .eq('user_id', userId) - - if (teamMemberError || !teamMemberData || teamMemberData.length === 0) { - return returnServerError('User is not a member of this team') - } - - const teamMember = teamMemberData[0]! - - if (teamMember.user_id !== user.id && teamMember.is_default) { - return returnServerError('Cannot remove a default team member') - } + const { teamId, session } = ctx - const { count, error: countError } = await supabaseAdmin - .from('users_teams') - .select('*', { count: 'exact', head: true }) - .eq('team_id', teamId) - - if (countError) { - return returnServerError( - `Error checking team members: ${countError.message}` - ) - } - - if (count === 1) { - return returnServerError('Cannot remove the last team member') - } - - const { error: removeError } = await supabaseAdmin - .from('users_teams') - .delete() - .eq('team_id', teamId) - .eq('user_id', userId) + const { error } = await api.DELETE('/teams/{teamId}/members/{userId}', { + params: { path: { teamId, userId } }, + headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), + }) - if (removeError) { - throw removeError + if (error) { + const message = + (error as { message?: string }).message ?? + 'Failed to remove team member' + return returnServerError(message) } - revalidateTag(CACHE_TAGS.USER_TEAM_AUTHORIZATION(userId, teamId), { - expire: 0, - }) revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') }) @@ -219,7 +152,7 @@ export const uploadTeamProfilePictureAction = authActionClient }) } - const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB in bytes + const MAX_FILE_SIZE = 5 * 1024 * 1024 if (image.size > MAX_FILE_SIZE) { return returnValidationErrors(UploadTeamProfilePictureSchema, { @@ -230,7 +163,6 @@ export const uploadTeamProfilePictureAction = authActionClient const arrayBuffer = await image.arrayBuffer() const buffer = Buffer.from(arrayBuffer) - // Verify actual file type using magic bytes (file signature) const fileType = await fileTypeFromBuffer(buffer) if (!fileType) { @@ -251,15 +183,13 @@ export const uploadTeamProfilePictureAction = authActionClient }) } - // Use the actual detected extension from file-type const extension = fileType.ext const fileName = `${Date.now()}.${extension}` - const filePath = `teams/${teamId}/${fileName}` + const storagePath = `teams/${teamId}/${fileName}` - // Upload file to Supabase Storage - const publicUrl = await uploadFile(buffer, filePath, fileType.mime) + const publicUrl = await uploadFile(buffer, storagePath, fileType.mime) - // Update team record with new profile picture URL + // profile_picture_url stays on supabase admin — tightly coupled to supabase storage const { data, error } = await supabaseAdmin .from('teams') .update({ profile_picture_url: publicUrl }) @@ -271,20 +201,14 @@ export const uploadTeamProfilePictureAction = authActionClient throw new Error(error.message) } - // Clean up old profile pictures asynchronously in the background after(async () => { try { - // Get the current file name from the path const currentFileName = fileName - - // List all files in the team's folder from Supabase Storage const folderPath = `teams/${teamId}` const files = await getFiles(folderPath) - // Delete all old profile pictures except the one we just uploaded for (const file of files) { const filePath = file.name - // Skip the file we just uploaded if (filePath === `${folderPath}/${currentFileName}`) { continue } diff --git a/src/server/team/types.ts b/src/server/team/types.ts index da7e90574..61441a2cf 100644 --- a/src/server/team/types.ts +++ b/src/server/team/types.ts @@ -1,6 +1,5 @@ import { z } from 'zod' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import type { Database } from '@/types/database.types' export type TeamMemberInfo = { id: string @@ -9,9 +8,14 @@ export type TeamMemberInfo = { avatar_url: string } +export type TeamMemberRelation = { + added_by: string | null + is_default: boolean +} + export type TeamMember = { info: TeamMemberInfo - relation: Database['public']['Tables']['users_teams']['Row'] + relation: TeamMemberRelation } /** diff --git a/src/types/dashboard-api.types.ts b/src/types/dashboard-api.types.ts index c19444bd9..f1528f078 100644 --- a/src/types/dashboard-api.types.ts +++ b/src/types/dashboard-api.types.ts @@ -4,412 +4,792 @@ */ export interface paths { - '/health': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Health check */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Health check successful */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['HealthResponse'] - } - } - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** List team builds */ - get: { - parameters: { - query?: { - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template?: components['parameters']['build_id_or_template'] - /** @description Comma-separated list of build statuses to include. */ - statuses?: components['parameters']['build_statuses'] - /** @description Maximum number of items to return per page. */ - limit?: components['parameters']['builds_limit'] - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - cursor?: components['parameters']['builds_cursor'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned paginated builds. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildsListResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds/statuses': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get build statuses */ - get: { - parameters: { - query: { - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: components['parameters']['build_ids'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned build statuses */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildsStatusesResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds/{build_id}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get build details */ - get: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the build. */ - build_id: components['parameters']['build_id'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned build details. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildInfo'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/record': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get sandbox record */ - get: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the sandbox. */ - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned sandbox details. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxRecord'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Health check successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List team builds */ + get: { + parameters: { + query?: { + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template?: components["parameters"]["build_id_or_template"]; + /** @description Comma-separated list of build statuses to include. */ + statuses?: components["parameters"]["build_statuses"]; + /** @description Maximum number of items to return per page. */ + limit?: components["parameters"]["builds_limit"]; + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + cursor?: components["parameters"]["builds_cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned paginated builds. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildsListResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds/statuses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get build statuses */ + get: { + parameters: { + query: { + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: components["parameters"]["build_ids"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned build statuses */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildsStatusesResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds/{build_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get build details */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the build. */ + build_id: components["parameters"]["build_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned build details. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildInfo"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/record": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get sandbox record */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the sandbox. */ + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned sandbox details. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxRecord"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List user teams + * @description Returns all teams the authenticated user belongs to, with limits and default flag. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned user teams. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserTeamsResponse"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/resolve": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Resolve team identity + * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. + */ + get: { + parameters: { + query: { + /** @description Team slug to resolve. */ + slug: components["parameters"]["teamSlug"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully resolved team. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamResolveResponse"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update team */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTeamRequest"]; + }; + }; + responses: { + /** @description Successfully updated team. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateTeamResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + trace?: never; + }; + "/teams/{teamId}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List team members */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned team members. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMembersResponse"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** Add team member */ + post: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AddTeamMemberRequest"]; + }; + }; + responses: { + /** @description Successfully added team member. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/members/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Remove team member */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + /** @description Identifier of the user. */ + userId: components["parameters"]["userId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully removed team member. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/defaults": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List default templates + * @description Returns the list of default templates with their latest build info and aliases. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned default templates. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DefaultTemplatesResponse"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } -export type webhooks = Record +export type webhooks = Record; export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code. - */ - code: number - /** @description Error message. */ - message: string - } - /** - * @description Build status mapped for dashboard clients. - * @enum {string} - */ - BuildStatus: 'building' | 'failed' | 'success' - ListedBuild: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string - /** @description Template alias when present, otherwise template ID. */ - template: string - /** @description Identifier of the template. */ - templateId: string - status: components['schemas']['BuildStatus'] - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - } - BuildsListResponse: { - data: components['schemas']['ListedBuild'][] - /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ - nextCursor: string | null - } - BuildStatusItem: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string - status: components['schemas']['BuildStatus'] - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - } - BuildsStatusesResponse: { - /** @description List of build statuses */ - buildStatuses: components['schemas']['BuildStatusItem'][] - } - BuildInfo: { - /** @description Template names related to this build, if available. */ - names?: string[] | null - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - status: components['schemas']['BuildStatus'] - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - } - /** - * Format: int64 - * @description CPU cores for the sandbox - */ - CPUCount: number - /** - * Format: int64 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number - /** - * Format: int64 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number - SandboxRecord: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string - /** @description Alias of the template */ - alias?: string - /** @description Identifier of the sandbox */ - sandboxID: string - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string - /** - * Format: date-time - * @description Time when the sandbox was stopped - */ - stoppedAt?: string | null - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - } - HealthResponse: { - /** @description Human-readable health check result. */ - message: string - } - } - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - } - parameters: { - /** @description Identifier of the build. */ - build_id: string - /** @description Identifier of the sandbox. */ - sandboxID: string - /** @description Maximum number of items to return per page. */ - builds_limit: number - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - builds_cursor: string - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template: string - /** @description Comma-separated list of build statuses to include. */ - build_statuses: components['schemas']['BuildStatus'][] - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: string[] - } - requestBodies: never - headers: never - pathItems: never + schemas: { + Error: { + /** + * Format: int32 + * @description Error code. + */ + code: number; + /** @description Error message. */ + message: string; + }; + /** + * @description Build status mapped for dashboard clients. + * @enum {string} + */ + BuildStatus: "building" | "failed" | "success"; + ListedBuild: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string; + /** @description Template alias when present, otherwise template ID. */ + template: string; + /** @description Identifier of the template. */ + templateId: string; + status: components["schemas"]["BuildStatus"]; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + }; + BuildsListResponse: { + data: components["schemas"]["ListedBuild"][]; + /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ + nextCursor: string | null; + }; + BuildStatusItem: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string; + status: components["schemas"]["BuildStatus"]; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + }; + BuildsStatusesResponse: { + /** @description List of build statuses */ + buildStatuses: components["schemas"]["BuildStatusItem"][]; + }; + BuildInfo: { + /** @description Template names related to this build, if available. */ + names?: string[] | null; + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + status: components["schemas"]["BuildStatus"]; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + }; + /** + * Format: int64 + * @description CPU cores for the sandbox + */ + CPUCount: number; + /** + * Format: int64 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number; + /** + * Format: int64 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number; + SandboxRecord: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string; + /** @description Alias of the template */ + alias?: string; + /** @description Identifier of the sandbox */ + sandboxID: string; + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string; + /** + * Format: date-time + * @description Time when the sandbox was stopped + */ + stoppedAt?: string | null; + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + }; + HealthResponse: { + /** @description Human-readable health check result. */ + message: string; + }; + UserTeamLimits: { + /** Format: int64 */ + maxLengthHours: number; + /** Format: int32 */ + concurrentSandboxes: number; + /** Format: int32 */ + concurrentTemplateBuilds: number; + /** Format: int32 */ + maxVcpu: number; + /** Format: int32 */ + maxRamMb: number; + /** Format: int32 */ + diskMb: number; + }; + UserTeam: { + /** Format: uuid */ + id: string; + name: string; + slug: string; + tier: string; + email: string; + isDefault: boolean; + limits: components["schemas"]["UserTeamLimits"]; + }; + UserTeamsResponse: { + teams: components["schemas"]["UserTeam"][]; + }; + TeamMember: { + /** Format: uuid */ + id: string; + email: string; + isDefault: boolean; + /** Format: uuid */ + addedBy?: string | null; + /** Format: date-time */ + createdAt: string | null; + }; + TeamMembersResponse: { + members: components["schemas"]["TeamMember"][]; + }; + UpdateTeamRequest: { + name: string; + }; + UpdateTeamResponse: { + /** Format: uuid */ + id: string; + name: string; + }; + AddTeamMemberRequest: { + /** Format: email */ + email: string; + }; + DefaultTemplateAlias: { + alias: string; + namespace?: string | null; + }; + DefaultTemplate: { + id: string; + aliases: components["schemas"]["DefaultTemplateAlias"][]; + /** Format: uuid */ + buildId: string; + /** Format: int64 */ + ramMb: number; + /** Format: int64 */ + vcpu: number; + /** Format: int64 */ + totalDiskSizeMb: number | null; + envdVersion?: string | null; + /** Format: date-time */ + createdAt: string; + public: boolean; + /** Format: int32 */ + buildCount: number; + /** Format: int64 */ + spawnCount: number; + }; + DefaultTemplatesResponse: { + templates: components["schemas"]["DefaultTemplate"][]; + }; + TeamResolveResponse: { + /** Format: uuid */ + id: string; + slug: string; + }; + }; + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: { + /** @description Identifier of the build. */ + build_id: string; + /** @description Identifier of the sandbox. */ + sandboxID: string; + /** @description Maximum number of items to return per page. */ + builds_limit: number; + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + builds_cursor: string; + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template: string; + /** @description Comma-separated list of build statuses to include. */ + build_statuses: components["schemas"]["BuildStatus"][]; + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: string[]; + /** @description Identifier of the team. */ + teamId: string; + /** @description Identifier of the user. */ + userId: string; + /** @description Team slug to resolve. */ + teamSlug: string; + }; + requestBodies: never; + headers: never; + pathItems: never; } -export type $defs = Record -export type operations = Record +export type $defs = Record; +export type operations = Record; From ffe5502114eb52dc359fbe4668bc984ee85271c1 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Tue, 17 Mar 2026 22:49:54 -0700 Subject: [PATCH 03/37] refactor: repository, transport, folder architecture --- src/__test__/integration/auth.test.ts | 9 +- .../integration/dashboard-route.test.ts | 2 +- .../integration/resolve-user-team.test.ts | 4 +- .../unit/fill-metrics-with-zeros.test.ts | 2 +- src/__test__/unit/sandbox-lifecycle.test.ts | 16 +- .../sandbox-monitoring-chart-model.test.ts | 14 +- src/app/(auth)/auth/cli/page.tsx | 28 +- src/app/(auth)/confirm/page.tsx | 4 +- src/app/(auth)/forgot-password/page.tsx | 4 +- src/app/(auth)/sign-in/page.tsx | 4 +- src/app/(auth)/sign-up/page.tsx | 4 +- src/app/api/auth/confirm/route.ts | 2 +- src/app/api/auth/verify-otp/route.ts | 10 +- src/app/api/teams/[teamId]/metrics/route.ts | 4 +- .../teams/[teamId]/sandboxes/metrics/route.ts | 4 +- src/app/api/teams/user/route.ts | 37 +- src/app/api/trpc/[trpc]/route.ts | 4 +- .../inspect/sandbox/[sandboxId]/route.ts | 14 +- src/app/dashboard/[teamIdOrSlug]/layout.tsx | 4 +- .../dashboard/[teamIdOrSlug]/team-gate.tsx | 11 +- .../dashboard/[teamIdOrSlug]/usage/page.tsx | 2 +- src/app/dashboard/account/route.ts | 2 +- src/app/dashboard/route.ts | 2 +- src/app/sbx/new/route.ts | 17 +- .../domains/auth/models.ts} | 0 src/core/domains/auth/repository.server.ts | 75 + src/core/domains/billing/repository.server.ts | 267 +++ .../domains/builds/models.ts} | 12 +- src/core/domains/builds/repository.server.ts | 284 +++ src/core/domains/keys/repository.server.ts | 99 ++ .../domains/sandboxes/models.ts} | 46 +- .../domains/sandboxes/repository.server.ts | 459 +++++ .../domains/sandboxes/schemas.ts} | 0 src/core/domains/support/repository.server.ts | 249 +++ src/core/domains/teams/models.ts | 31 + src/core/domains/teams/repository.server.ts | 333 ++++ src/core/domains/teams/schemas.ts | 23 + .../domains/templates/repository.server.ts | 193 ++ .../domains/webhooks/repository.server.ts | 168 ++ .../server/actions}/auth-actions.ts | 14 +- .../server/actions}/key-actions.ts | 51 +- .../server/actions}/sandbox-actions.ts | 0 .../server/actions}/team-actions.ts | 67 +- .../server/actions}/user-actions.ts | 0 .../server/actions}/webhooks-actions.ts | 98 +- src/core/server/adapters/repo-error.ts | 35 + .../server/adapters/trpc-errors.ts} | 0 src/{ => core}/server/api/middlewares/auth.ts | 16 +- .../server/api/middlewares/telemetry.ts | 4 +- src/core/server/api/routers/billing.ts | 130 ++ src/{ => core}/server/api/routers/builds.ts | 81 +- src/{ => core}/server/api/routers/index.ts | 2 +- src/{ => core}/server/api/routers/sandbox.ts | 71 +- src/core/server/api/routers/sandboxes.ts | 156 ++ src/{ => core}/server/api/routers/support.ts | 14 +- src/core/server/api/routers/teams.ts | 20 + src/core/server/api/routers/templates.ts | 75 + src/core/server/context/from-route.ts | 11 + src/core/server/context/request-context.ts | 68 + .../server/functions}/auth/auth.types.ts | 0 .../server/functions}/auth/get-session.ts | 0 .../functions}/auth/get-user-by-token.ts | 0 .../server/functions}/auth/validate-email.ts | 0 .../server/functions}/keys/get-api-keys.ts | 18 +- .../server/functions}/keys/types.ts | 0 .../sandboxes/get-team-metrics-core.ts | 2 +- .../sandboxes/get-team-metrics-max.ts | 0 .../functions}/sandboxes/get-team-metrics.ts | 0 .../server/functions}/sandboxes/utils.ts | 0 .../team/get-team-id-from-segment.ts | 15 +- .../server/functions}/team/get-team-limits.ts | 30 +- .../server/functions/team/get-team-members.ts | 22 + src/core/server/functions/team/get-team.ts | 37 + .../functions}/team/resolve-user-team.ts | 19 +- src/core/server/functions/team/types.ts | 11 + src/core/server/functions/usage/get-usage.ts | 32 + .../functions}/webhooks/get-webhooks.ts | 27 +- .../server/functions}/webhooks/schema.ts | 0 src/{server => core/server/http}/proxy.ts | 0 src/{server/api => core/server/trpc}/init.ts | 6 + .../api => core/server/trpc}/procedures.ts | 19 +- src/core/shared/errors.ts | 59 + src/core/shared/result.ts | 37 + src/core/shared/schemas.ts | 3 + src/features/auth/oauth-provider-buttons.tsx | 2 +- .../dashboard/account/email-settings.tsx | 2 +- .../dashboard/account/name-settings.tsx | 2 +- .../dashboard/account/password-settings.tsx | 2 +- .../dashboard/account/reauth-dialog.tsx | 2 +- .../dashboard/account/user-access-token.tsx | 2 +- .../dashboard/billing/selected-plan.tsx | 2 +- src/features/dashboard/billing/types.ts | 2 +- .../dashboard/build/build-logs-store.ts | 4 +- src/features/dashboard/build/header.tsx | 8 +- src/features/dashboard/build/logs-cells.tsx | 6 +- .../dashboard/build/logs-filter-params.ts | 4 +- src/features/dashboard/build/logs.tsx | 18 +- .../dashboard/build/use-build-logs.ts | 2 +- .../dashboard/members/add-member-form.tsx | 2 +- .../dashboard/members/danger-zone.tsx | 2 +- .../dashboard/members/member-table-body.tsx | 2 +- .../dashboard/members/member-table-row.tsx | 4 +- src/features/dashboard/sandbox/context.tsx | 18 +- .../dashboard/sandbox/header/kill-button.tsx | 2 +- .../dashboard/sandbox/logs/logs-cells.tsx | 6 +- .../sandbox/logs/logs-filter-params.ts | 4 +- src/features/dashboard/sandbox/logs/logs.tsx | 10 +- .../sandbox/logs/sandbox-logs-store.ts | 4 +- .../use-sandbox-monitoring-controller.ts | 2 +- .../monitoring/utils/chart-lifecycle.ts | 10 +- .../sandbox/monitoring/utils/chart-metrics.ts | 2 +- .../sandbox/monitoring/utils/chart-model.ts | 6 +- .../sandbox/monitoring/utils/timeframe.ts | 4 +- .../sandboxes/live-counter.client.tsx | 2 +- .../sandboxes/live-counter.server.tsx | 2 +- .../sandboxes/monitoring/charts/charts.tsx | 4 +- .../sandboxes/monitoring/header.client.tsx | 2 +- .../dashboard/sandboxes/monitoring/header.tsx | 6 +- .../dashboard/settings/general/name-card.tsx | 4 +- .../settings/general/profile-picture-card.tsx | 2 +- .../settings/keys/create-api-key-dialog.tsx | 2 +- .../dashboard/settings/keys/table-body.tsx | 2 +- .../dashboard/settings/keys/table-row.tsx | 2 +- .../webhooks/add-edit-dialog-steps.tsx | 2 +- .../settings/webhooks/add-edit-dialog.tsx | 6 +- .../settings/webhooks/delete-dialog.tsx | 2 +- .../settings/webhooks/edit-secret-dialog.tsx | 4 +- .../settings/webhooks/table-body.tsx | 2 +- .../dashboard/sidebar/create-team-dialog.tsx | 4 +- src/features/dashboard/sidebar/menu.tsx | 2 +- .../dashboard/templates/builds/constants.ts | 2 +- .../dashboard/templates/builds/header.tsx | 2 +- .../templates/builds/table-cells.tsx | 10 +- .../dashboard/templates/builds/table.tsx | 14 +- .../templates/builds/use-filters.tsx | 2 +- src/lib/clients/action.ts | 32 +- src/lib/utils/trpc-errors.ts | 2 +- src/proxy.ts | 2 +- .../api/repositories/auth.repository.ts | 112 -- .../api/repositories/builds.repository.ts | 279 --- .../api/repositories/sandboxes.repository.ts | 302 ---- .../api/repositories/support.repository.ts | 325 ---- src/server/api/routers/billing.ts | 413 ----- src/server/api/routers/sandboxes.ts | 283 --- src/server/api/routers/teams.ts | 63 - src/server/api/routers/templates.ts | 237 --- src/server/team/get-team-members.ts | 59 - src/server/team/get-team.ts | 83 - src/server/team/types.ts | 55 - src/server/usage/get-usage.ts | 58 - src/trpc/client.tsx | 2 +- src/trpc/server.tsx | 5 +- src/types/dashboard-api.types.ts | 1572 ++++++++--------- tsconfig.json | 27 + 154 files changed, 4157 insertions(+), 3663 deletions(-) rename src/{server/api/models/auth.models.ts => core/domains/auth/models.ts} (100%) create mode 100644 src/core/domains/auth/repository.server.ts create mode 100644 src/core/domains/billing/repository.server.ts rename src/{server/api/models/builds.models.ts => core/domains/builds/models.ts} (86%) create mode 100644 src/core/domains/builds/repository.server.ts create mode 100644 src/core/domains/keys/repository.server.ts rename src/{server/api/models/sandboxes.models.ts => core/domains/sandboxes/models.ts} (82%) create mode 100644 src/core/domains/sandboxes/repository.server.ts rename src/{server/api/schemas/sandboxes.ts => core/domains/sandboxes/schemas.ts} (100%) create mode 100644 src/core/domains/support/repository.server.ts create mode 100644 src/core/domains/teams/models.ts create mode 100644 src/core/domains/teams/repository.server.ts create mode 100644 src/core/domains/teams/schemas.ts create mode 100644 src/core/domains/templates/repository.server.ts create mode 100644 src/core/domains/webhooks/repository.server.ts rename src/{server/auth => core/server/actions}/auth-actions.ts (98%) rename src/{server/keys => core/server/actions}/key-actions.ts (65%) rename src/{server/sandboxes => core/server/actions}/sandbox-actions.ts (100%) rename src/{server/team => core/server/actions}/team-actions.ts (76%) rename src/{server/user => core/server/actions}/user-actions.ts (100%) rename src/{server/webhooks => core/server/actions}/webhooks-actions.ts (63%) create mode 100644 src/core/server/adapters/repo-error.ts rename src/{server/api/errors.ts => core/server/adapters/trpc-errors.ts} (100%) rename src/{ => core}/server/api/middlewares/auth.ts (77%) rename src/{ => core}/server/api/middlewares/telemetry.ts (98%) create mode 100644 src/core/server/api/routers/billing.ts rename src/{ => core}/server/api/routers/builds.ts (66%) rename src/{ => core}/server/api/routers/index.ts (91%) rename src/{ => core}/server/api/routers/sandbox.ts (71%) create mode 100644 src/core/server/api/routers/sandboxes.ts rename src/{ => core}/server/api/routers/support.ts (74%) create mode 100644 src/core/server/api/routers/teams.ts create mode 100644 src/core/server/api/routers/templates.ts create mode 100644 src/core/server/context/from-route.ts create mode 100644 src/core/server/context/request-context.ts rename src/{server => core/server/functions}/auth/auth.types.ts (100%) rename src/{server => core/server/functions}/auth/get-session.ts (100%) rename src/{server => core/server/functions}/auth/get-user-by-token.ts (100%) rename src/{server => core/server/functions}/auth/validate-email.ts (100%) rename src/{server => core/server/functions}/keys/get-api-keys.ts (72%) rename src/{server => core/server/functions}/keys/types.ts (100%) rename src/{server => core/server/functions}/sandboxes/get-team-metrics-core.ts (97%) rename src/{server => core/server/functions}/sandboxes/get-team-metrics-max.ts (100%) rename src/{server => core/server/functions}/sandboxes/get-team-metrics.ts (100%) rename src/{server => core/server/functions}/sandboxes/utils.ts (100%) rename src/{server => core/server/functions}/team/get-team-id-from-segment.ts (69%) rename src/{server => core/server/functions}/team/get-team-limits.ts (52%) create mode 100644 src/core/server/functions/team/get-team-members.ts create mode 100644 src/core/server/functions/team/get-team.ts rename src/{server => core/server/functions}/team/resolve-user-team.ts (65%) create mode 100644 src/core/server/functions/team/types.ts create mode 100644 src/core/server/functions/usage/get-usage.ts rename src/{server => core/server/functions}/webhooks/get-webhooks.ts (57%) rename src/{server => core/server/functions}/webhooks/schema.ts (100%) rename src/{server => core/server/http}/proxy.ts (100%) rename src/{server/api => core/server/trpc}/init.ts (70%) rename src/{server/api => core/server/trpc}/procedures.ts (84%) create mode 100644 src/core/shared/errors.ts create mode 100644 src/core/shared/result.ts create mode 100644 src/core/shared/schemas.ts delete mode 100644 src/server/api/repositories/auth.repository.ts delete mode 100644 src/server/api/repositories/builds.repository.ts delete mode 100644 src/server/api/repositories/sandboxes.repository.ts delete mode 100644 src/server/api/repositories/support.repository.ts delete mode 100644 src/server/api/routers/billing.ts delete mode 100644 src/server/api/routers/sandboxes.ts delete mode 100644 src/server/api/routers/teams.ts delete mode 100644 src/server/api/routers/templates.ts delete mode 100644 src/server/team/get-team-members.ts delete mode 100644 src/server/team/get-team.ts delete mode 100644 src/server/team/types.ts delete mode 100644 src/server/usage/get-usage.ts diff --git a/src/__test__/integration/auth.test.ts b/src/__test__/integration/auth.test.ts index 8b9070347..ff3cdfab3 100644 --- a/src/__test__/integration/auth.test.ts +++ b/src/__test__/integration/auth.test.ts @@ -1,14 +1,14 @@ import { redirect } from 'next/navigation' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { encodedRedirect } from '@/lib/utils/auth' import { forgotPasswordAction, signInAction, signInWithOAuthAction, signOutAction, signUpAction, -} from '@/server/auth/auth-actions' +} from '@/core/server/actions/auth-actions' +import { encodedRedirect } from '@/lib/utils/auth' // Create hoisted mock functions that can be used throughout the file const { validateEmail, shouldWarnAboutAlternateEmail } = vi.hoisted(() => ({ @@ -77,7 +77,7 @@ vi.mock('@/lib/utils/auth', () => ({ })) // Use the hoisted mock functions in the module mock -vi.mock('@/server/auth/validate-email', () => ({ +vi.mock('@/core/server/functions/auth/validate-email', () => ({ validateEmail, shouldWarnAboutAlternateEmail, })) @@ -266,10 +266,9 @@ describe('Auth Actions - Integration Tests', () => { // Missing password and confirmPassword // Execute: Call the sign-up action - // @ts-expect-error - we want to test the validation errors const result = await signUpAction({ email: 'newuser@example.com', - }) + } as never) // Verify: Check that the result contains validation errors expect(result).toBeDefined() diff --git a/src/__test__/integration/dashboard-route.test.ts b/src/__test__/integration/dashboard-route.test.ts index 0dee60fce..9b8c328a8 100644 --- a/src/__test__/integration/dashboard-route.test.ts +++ b/src/__test__/integration/dashboard-route.test.ts @@ -63,7 +63,7 @@ vi.mock('@/lib/utils/auth', () => ({ }), })) -vi.mock('@/server/team/resolve-user-team', () => ({ +vi.mock('@/core/server/functions/team/resolve-user-team', () => ({ resolveUserTeam: mockResolveUserTeam, })) diff --git a/src/__test__/integration/resolve-user-team.test.ts b/src/__test__/integration/resolve-user-team.test.ts index 8c26c2cdc..bcd146bb6 100644 --- a/src/__test__/integration/resolve-user-team.test.ts +++ b/src/__test__/integration/resolve-user-team.test.ts @@ -40,12 +40,12 @@ vi.mock('next/headers', () => ({ cookies: vi.fn(() => mockCookieStore), })) -vi.mock('@/server/auth/check-user-team-auth-cached', () => ({ +vi.mock('@/core/server/functions/auth/check-user-team-auth-cached', () => ({ checkUserTeamAuth: mockCheckUserTeamAuth, })) // import after mocks are set up -import { resolveUserTeam } from '@/server/team/resolve-user-team' +import { resolveUserTeam } from '@/core/server/functions/team/resolve-user-team' describe('resolveUserTeam - Authorization Integration Tests', () => { beforeEach(() => { diff --git a/src/__test__/unit/fill-metrics-with-zeros.test.ts b/src/__test__/unit/fill-metrics-with-zeros.test.ts index 40af27022..9fd3699e6 100644 --- a/src/__test__/unit/fill-metrics-with-zeros.test.ts +++ b/src/__test__/unit/fill-metrics-with-zeros.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { fillTeamMetricsWithZeros } from '@/server/sandboxes/utils' +import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' import type { ClientTeamMetrics } from '@/types/sandboxes.types' describe('fillTeamMetricsWithZeros', () => { diff --git a/src/__test__/unit/sandbox-lifecycle.test.ts b/src/__test__/unit/sandbox-lifecycle.test.ts index 4c0cc9eab..4ac095579 100644 --- a/src/__test__/unit/sandbox-lifecycle.test.ts +++ b/src/__test__/unit/sandbox-lifecycle.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest' import { deriveSandboxLifecycleFromEvents, - type SandboxEventDTO, -} from '@/server/api/models/sandboxes.models' + type SandboxEventModel, +} from '@/core/domains/sandboxes/models' function createLifecycleEvent( - overrides: Partial & Pick -): SandboxEventDTO { + overrides: Partial & Pick +): SandboxEventModel { return { id: overrides.id, version: 'v1', @@ -24,7 +24,7 @@ function createLifecycleEvent( describe('deriveSandboxLifecycleFromEvents', () => { it('derives createdAt, pausedAt and endedAt from a full lifecycle', () => { - const events: SandboxEventDTO[] = [ + const events: SandboxEventModel[] = [ createLifecycleEvent({ id: '5', type: 'sandbox.lifecycle.killed', @@ -68,7 +68,7 @@ describe('deriveSandboxLifecycleFromEvents', () => { }) it('keeps pausedAt when the sandbox is paused and not resumed yet', () => { - const events: SandboxEventDTO[] = [ + const events: SandboxEventModel[] = [ createLifecycleEvent({ id: '1', type: 'sandbox.lifecycle.created', @@ -89,7 +89,7 @@ describe('deriveSandboxLifecycleFromEvents', () => { }) it('uses first created event and latest killed event', () => { - const events: SandboxEventDTO[] = [ + const events: SandboxEventModel[] = [ createLifecycleEvent({ id: '1', type: 'sandbox.lifecycle.created', @@ -119,7 +119,7 @@ describe('deriveSandboxLifecycleFromEvents', () => { }) it('ignores non-lifecycle events', () => { - const events: SandboxEventDTO[] = [ + const events: SandboxEventModel[] = [ createLifecycleEvent({ id: '1', type: 'sandbox.lifecycle.created', diff --git a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts index 8b7a4f20b..17315728d 100644 --- a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts +++ b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest' -import { buildMonitoringChartModel } from '@/features/dashboard/sandbox/monitoring/utils/chart-model' import type { - SandboxEventDTO, + SandboxEventModel, SandboxMetric, -} from '@/server/api/models/sandboxes.models' +} from '@/core/domains/sandboxes/models' +import { buildMonitoringChartModel } from '@/features/dashboard/sandbox/monitoring/utils/chart-model' const baseMetric = { timestamp: '1970-01-01T00:00:00.000Z', @@ -16,8 +16,8 @@ const baseMetric = { > function createLifecycleEvent( - overrides: Partial & Pick -): SandboxEventDTO { + overrides: Partial & Pick +): SandboxEventModel { return { id: overrides.id, version: 'v1', @@ -148,7 +148,7 @@ describe('buildMonitoringChartModel', () => { }, ] - const lifecycleEvents: SandboxEventDTO[] = [ + const lifecycleEvents: SandboxEventModel[] = [ createLifecycleEvent({ id: 'pause', type: 'sandbox.lifecycle.paused', @@ -216,7 +216,7 @@ describe('buildMonitoringChartModel', () => { }) it('builds visible lifecycle event markers for created, paused, resumed, and killed only', () => { - const lifecycleEvents: SandboxEventDTO[] = [ + const lifecycleEvents: SandboxEventModel[] = [ createLifecycleEvent({ id: 'outside', type: 'sandbox.lifecycle.killed', diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 8b57dca86..0ffb86191 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -2,9 +2,8 @@ import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react' import { redirect } from 'next/navigation' import { Suspense } from 'react' import { serializeError } from 'serialize-error' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { api } from '@/lib/clients/api' +import { createTeamsRepository } from '@/domains/teams/repository.server' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' @@ -29,13 +28,18 @@ async function handleCLIAuth( throw new Error('Invalid redirect URL') } - const { data: teamsData, error: teamsError } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(supabaseAccessToken), - }) + const teamsResult = await createTeamsRepository({ + accessToken: supabaseAccessToken, + }).listUserTeams() + + if (!teamsResult.ok) { + throw new Error('Failed to resolve default team') + } - const defaultTeam = teamsData?.teams.find((t) => t.isDefault) + const defaultTeam = + teamsResult.data.find((team) => team.is_default) ?? teamsResult.data[0] - if (teamsError || !defaultTeam) { + if (!defaultTeam) { throw new Error('Failed to resolve default team') } @@ -136,11 +140,11 @@ export default async function CLIAuthPage({ throw new Error('No provider access token found') } - return await handleCLIAuth( - next, - user.email!, - session.access_token - ) + if (!user.email) { + throw new Error('No user email found') + } + + return await handleCLIAuth(next, user.email, session.access_token) } catch (err) { if (err instanceof Error && err.message.includes('NEXT_REDIRECT')) { throw err diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 7cfaf88f9..2f0b9e4d4 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -4,13 +4,13 @@ import { useMutation } from '@tanstack/react-query' import { useRouter, useSearchParams } from 'next/navigation' import { useMemo, useTransition } from 'react' import { AUTH_URLS } from '@/configs/urls' -import { AuthFormMessage } from '@/features/auth/form-message' import { type ConfirmEmailInput, ConfirmEmailInputSchema, type OtpType, OtpTypeSchema, -} from '@/server/api/models/auth.models' +} from '@/core/domains/auth/models' +import { AuthFormMessage } from '@/features/auth/form-message' import { Button } from '@/ui/primitives/button' const OTP_TYPE_LABELS: Record = { diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index dc470821e..3aa518ee9 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -9,9 +9,9 @@ import { getTimeoutMsFromUserMessage, USER_MESSAGES, } from '@/configs/user-messages' +import { forgotPasswordAction } from '@/core/server/actions/auth-actions' +import { forgotPasswordSchema } from '@/core/server/functions/auth/auth.types' import { AuthFormMessage, type AuthMessage } from '@/features/auth/form-message' -import { forgotPasswordSchema } from '@/server/auth/auth.types' -import { forgotPasswordAction } from '@/server/auth/auth-actions' import { Button } from '@/ui/primitives/button' import { Input } from '@/ui/primitives/input' import { Label } from '@/ui/primitives/label' diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index d091d7674..7bc10651f 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -7,10 +7,10 @@ import { useSearchParams } from 'next/navigation' import { Suspense, useEffect, useState } from 'react' import { AUTH_URLS } from '@/configs/urls' import { USER_MESSAGES } from '@/configs/user-messages' +import { signInAction } from '@/core/server/actions/auth-actions' +import { signInSchema } from '@/core/server/functions/auth/auth.types' import { AuthFormMessage, type AuthMessage } from '@/features/auth/form-message' import { OAuthProviders } from '@/features/auth/oauth-provider-buttons' -import { signInSchema } from '@/server/auth/auth.types' -import { signInAction } from '@/server/auth/auth-actions' import { Button } from '@/ui/primitives/button' import { Form, diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx index 7fdad1eb8..e1022c484 100644 --- a/src/app/(auth)/sign-up/page.tsx +++ b/src/app/(auth)/sign-up/page.tsx @@ -11,12 +11,12 @@ import { getTimeoutMsFromUserMessage, USER_MESSAGES, } from '@/configs/user-messages' +import { signUpAction } from '@/core/server/actions/auth-actions' +import { signUpSchema } from '@/core/server/functions/auth/auth.types' import { AuthFormMessage, type AuthMessage } from '@/features/auth/form-message' import { OAuthProviders } from '@/features/auth/oauth-provider-buttons' import { TurnstileWidget } from '@/features/auth/turnstile-widget' import { useTurnstile } from '@/features/auth/use-turnstile' -import { signUpSchema } from '@/server/auth/auth.types' -import { signUpAction } from '@/server/auth/auth-actions' import { Button } from '@/ui/primitives/button' import { Form, diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index ee0033f98..3f9a7ea03 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -2,9 +2,9 @@ import { redirect } from 'next/navigation' import type { NextRequest } from 'next/server' import { z } from 'zod' import { AUTH_URLS } from '@/configs/urls' +import { OtpTypeSchema } from '@/core/domains/auth/models' import { l } from '@/lib/clients/logger/logger' import { encodedRedirect, isExternalOrigin } from '@/lib/utils/auth' -import { OtpTypeSchema } from '@/server/api/models/auth.models' const confirmSchema = z.object({ token_hash: z.string().min(1), diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 87f3cd02d..521ba79aa 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -1,13 +1,13 @@ import { type NextRequest, NextResponse } from 'next/server' import { flattenError } from 'zod' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { l } from '@/lib/clients/logger/logger' -import { isExternalOrigin } from '@/lib/utils/auth' import { ConfirmEmailInputSchema, type OtpType, -} from '@/server/api/models/auth.models' -import { authRepo } from '@/server/api/repositories/auth.repository' +} from '@/core/domains/auth/models' +import { authRepository } from '@/domains/auth/repository.server' +import { l } from '@/lib/clients/logger/logger' +import { isExternalOrigin } from '@/lib/utils/auth' /** * Determines the redirect URL based on OTP type. @@ -109,7 +109,7 @@ export async function POST(request: NextRequest) { `verifying OTP token: ${token_hash.slice(0, 10)}` ) - await authRepo.verifyOtp(token_hash, type) + await authRepository.verifyOtp(token_hash, type) const redirectUrl = buildRedirectUrl(type, next, origin) diff --git a/src/app/api/teams/[teamId]/metrics/route.ts b/src/app/api/teams/[teamId]/metrics/route.ts index 3223e779a..415ec4003 100644 --- a/src/app/api/teams/[teamId]/metrics/route.ts +++ b/src/app/api/teams/[teamId]/metrics/route.ts @@ -1,9 +1,9 @@ import 'server-cli-only' import { serializeError } from 'serialize-error' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import { getTeamMetricsCore } from '@/core/server/functions/sandboxes/get-team-metrics-core' import { l } from '@/lib/clients/logger/logger' -import { getSessionInsecure } from '@/server/auth/get-session' -import { getTeamMetricsCore } from '@/server/sandboxes/get-team-metrics-core' import { TeamMetricsRequestSchema, type TeamMetricsResponse } from './types' export async function POST( diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts b/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts index cb410c4fc..384521645 100644 --- a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts +++ b/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts @@ -1,11 +1,11 @@ import 'server-cli-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import { transformMetricsToClientMetrics } from '@/core/server/functions/sandboxes/utils' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' -import { getSessionInsecure } from '@/server/auth/get-session' -import { transformMetricsToClientMetrics } from '@/server/sandboxes/utils' import { MetricsRequestSchema, type MetricsResponse } from './types' export async function POST( diff --git a/src/app/api/teams/user/route.ts b/src/app/api/teams/user/route.ts index c49e61d8a..ea10a0fac 100644 --- a/src/app/api/teams/user/route.ts +++ b/src/app/api/teams/user/route.ts @@ -1,8 +1,6 @@ -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { api } from '@/lib/clients/api' +import { createRouteServices } from '@/core/server/context/from-route' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { createClient } from '@/lib/clients/supabase/server' -import { getSessionInsecure } from '@/server/auth/get-session' -import type { ClientTeam } from '@/types/dashboard.types' import type { UserTeamsResponse } from './types' export async function GET() { @@ -14,33 +12,16 @@ export async function GET() { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(session.access_token), - }) + const services = createRouteServices({ accessToken: session.access_token }) + const teamsResult = await services.teams.listUserTeams() - if (error || !data?.teams) { - return Response.json( - { error: 'Failed to fetch teams' }, - { status: 500 } - ) + if (!teamsResult.ok) { + return Response.json({ error: 'Failed to fetch teams' }, { status: 500 }) } - const teams: ClientTeam[] = data.teams.map((t) => ({ - id: t.id, - name: t.name, - slug: t.slug, - tier: t.tier, - email: t.email, - is_default: t.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: null, - })) - - return Response.json({ teams } satisfies UserTeamsResponse) + return Response.json({ + teams: teamsResult.data, + } satisfies UserTeamsResponse) } catch (error) { if ( error instanceof Error && diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts index 303157cac..ee154b2c6 100644 --- a/src/app/api/trpc/[trpc]/route.ts +++ b/src/app/api/trpc/[trpc]/route.ts @@ -1,8 +1,8 @@ import { fetchRequestHandler } from '@trpc/server/adapters/fetch' import type { NextRequest } from 'next/server' -import { createTRPCContext } from '@/server/api/init' -import { trpcAppRouter } from '@/server/api/routers' +import { trpcAppRouter } from '@/core/server/api/routers' +import { createTRPCContext } from '@/core/server/trpc/init' /** * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when diff --git a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts index 47f79b721..223d83b3c 100644 --- a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts +++ b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts @@ -4,7 +4,8 @@ import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { api, infra } from '@/lib/clients/api' +import { createRouteServices } from '@/core/server/context/from-route' +import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { SandboxIdSchema } from '@/lib/schemas/api' @@ -158,16 +159,15 @@ export async function GET( } const accessToken = sessionResponse.session.access_token - const { data: teamsData, error: teamsError } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(accessToken), - }) + const services = createRouteServices({ accessToken }) + const teamsResult = await services.teams.listUserTeams() - if (teamsError || !teamsData?.teams || teamsData.teams.length === 0) { + if (!teamsResult.ok || teamsResult.data.length === 0) { l.warn({ key: 'inspect_sandbox:teams_fetch_error', user_id: userId, sandbox_id: sandboxId, - error: teamsError, + error: !teamsResult.ok ? teamsResult.error : undefined, }) return redirectToDashboardWithWarning( @@ -180,7 +180,7 @@ export async function GET( ) } - const userTeams: UserTeam[] = teamsData.teams.map((team) => ({ + const userTeams: UserTeam[] = teamsResult.data.map((team) => ({ id: team.id, slug: team.slug, })) diff --git a/src/app/dashboard/[teamIdOrSlug]/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/layout.tsx index 68f8b001f..c8e461bf2 100644 --- a/src/app/dashboard/[teamIdOrSlug]/layout.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/layout.tsx @@ -5,10 +5,10 @@ import { DashboardTeamGate } from '@/app/dashboard/[teamIdOrSlug]/team-gate' import { COOKIE_KEYS } from '@/configs/cookies' import { METADATA } from '@/configs/metadata' import { AUTH_URLS } from '@/configs/urls' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import DashboardLayoutView from '@/features/dashboard/layouts/layout' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { getSessionInsecure } from '@/server/auth/get-session' -import getUserByToken from '@/server/auth/get-user-by-token' import { HydrateClient, prefetch, prefetchAsync, trpc } from '@/trpc/server' import { SidebarInset, SidebarProvider } from '@/ui/primitives/sidebar' diff --git a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx index 9bb8d3e55..86dd7f6f7 100644 --- a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx @@ -1,7 +1,10 @@ 'use client' import type { User } from '@supabase/supabase-js' -import { QueryErrorResetBoundary, useSuspenseQuery } from '@tanstack/react-query' +import { + QueryErrorResetBoundary, + useSuspenseQuery, +} from '@tanstack/react-query' import { Suspense } from 'react' import { ErrorBoundary } from 'react-error-boundary' import { DashboardContextProvider } from '@/features/dashboard/context' @@ -14,11 +17,7 @@ interface DashboardTeamGateProps { children: React.ReactNode } -function TeamContent({ - teamIdOrSlug, - user, - children, -}: DashboardTeamGateProps) { +function TeamContent({ teamIdOrSlug, user, children }: DashboardTeamGateProps) { const trpc = useTRPC() const { data: team } = useSuspenseQuery( diff --git a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx b/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx index 220c0c330..6b3f56b07 100644 --- a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx @@ -1,6 +1,6 @@ +import { getUsage } from '@/core/server/functions/usage/get-usage' import { UsageChartsProvider } from '@/features/dashboard/usage/usage-charts-context' import { UsageMetricChart } from '@/features/dashboard/usage/usage-metric-chart' -import { getUsage } from '@/server/usage/get-usage' import ErrorBoundary from '@/ui/error' import Frame from '@/ui/frame' diff --git a/src/app/dashboard/account/route.ts b/src/app/dashboard/account/route.ts index 423f2bcc8..f8a87d0fb 100644 --- a/src/app/dashboard/account/route.ts +++ b/src/app/dashboard/account/route.ts @@ -1,9 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' -import { getSessionInsecure } from '@/server/auth/get-session' import { resolveUserTeam } from '@/server/team/resolve-user-team' export async function GET(request: NextRequest) { diff --git a/src/app/dashboard/route.ts b/src/app/dashboard/route.ts index 61e46a557..b3c4d8a22 100644 --- a/src/app/dashboard/route.ts +++ b/src/app/dashboard/route.ts @@ -1,9 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' -import { getSessionInsecure } from '@/server/auth/get-session' import { resolveUserTeam } from '@/server/team/resolve-user-team' export const TAB_URL_MAP: Record string> = { diff --git a/src/app/sbx/new/route.ts b/src/app/sbx/new/route.ts index 05eed993a..470188c69 100644 --- a/src/app/sbx/new/route.ts +++ b/src/app/sbx/new/route.ts @@ -3,10 +3,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { api } from '@/lib/clients/api' +import { createRouteServices } from '@/core/server/context/from-route' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' -import { getSessionInsecure } from '@/server/auth/get-session' export const GET = async (req: NextRequest) => { try { @@ -35,13 +35,14 @@ export const GET = async (req: NextRequest) => { ) } - const { data: teamsData, error: teamsError } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(session.access_token), - }) - - const defaultTeam = teamsData?.teams.find((t) => t.isDefault) + const services = createRouteServices({ accessToken: session.access_token }) + const teamsResult = await services.teams.listUserTeams() + const defaultTeam = teamsResult.ok + ? (teamsResult.data.find((team) => team.is_default) ?? + teamsResult.data[0]) + : null - if (teamsError || !defaultTeam) { + if (!defaultTeam) { return NextResponse.redirect(new URL(req.url).origin) } diff --git a/src/server/api/models/auth.models.ts b/src/core/domains/auth/models.ts similarity index 100% rename from src/server/api/models/auth.models.ts rename to src/core/domains/auth/models.ts diff --git a/src/core/domains/auth/repository.server.ts b/src/core/domains/auth/repository.server.ts new file mode 100644 index 000000000..3fd4bd0fa --- /dev/null +++ b/src/core/domains/auth/repository.server.ts @@ -0,0 +1,75 @@ +import 'server-only' + +import { TRPCError } from '@trpc/server' +import { serializeError } from 'serialize-error' +import type { OtpType } from '@/core/domains/auth/models' +import { l } from '@/lib/clients/logger/logger' +import { createClient } from '@/lib/clients/supabase/server' + +interface VerifyOtpResult { + userId: string +} + +type AuthRepositoryDeps = { + createSupabaseClient: typeof createClient +} + +export interface AuthRepository { + verifyOtp(tokenHash: string, type: OtpType): Promise +} + +export function createAuthRepository(deps: AuthRepositoryDeps): AuthRepository { + return { + async verifyOtp(tokenHash, type) { + const supabase = await deps.createSupabaseClient() + + const { data, error } = await supabase.auth.verifyOtp({ + type, + token_hash: tokenHash, + }) + + if (error) { + l.error( + { + key: 'auth_repository:verify_otp:error', + error: serializeError(error), + context: { + type, + token_hash_prefix: tokenHash.slice(0, 10), + error_code: error.code, + error_status: error.status, + }, + }, + `failed to verify OTP: ${error.message}` + ) + + if (error.status === 403 && error.code === 'otp_expired') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Email link has expired. Please request a new one.', + }) + } + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Invalid or expired verification link.', + }) + } + + if (!data.user) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Verification failed. Please try again.', + }) + } + + return { + userId: data.user.id, + } + }, + } +} + +export const authRepository = createAuthRepository({ + createSupabaseClient: createClient, +}) diff --git a/src/core/domains/billing/repository.server.ts b/src/core/domains/billing/repository.server.ts new file mode 100644 index 000000000..d1347b6ed --- /dev/null +++ b/src/core/domains/billing/repository.server.ts @@ -0,0 +1,267 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' +import type { + AddOnOrderConfirmResponse, + AddOnOrderCreateResponse, + BillingLimit, + CustomerPortalResponse, + Invoice, + PaymentMethodsCustomerSession, + TeamItems, + UsageResponse, +} from '@/types/billing.types' + +type BillingRepositoryDeps = { + billingApiUrl: string +} + +export interface BillingScope { + accessToken: string + teamId: string +} + +export interface BillingRepository { + createCheckout(tierId: string): Promise> + createCustomerPortalSession( + origin?: string | null + ): Promise> + getItems(): Promise> + getUsage(): Promise> + getInvoices(): Promise> + getLimits(): Promise> + setLimit(key: string, value: number): Promise> + clearLimit(key: string): Promise> + createOrder(itemId: string): Promise> + confirmOrder(orderId: string): Promise> + getCustomerSession(): Promise> +} + +async function parseText(response: Response): Promise { + return (await response.text()) || 'Request failed' +} + +export function createBillingRepository( + scope: BillingScope, + deps: BillingRepositoryDeps = { + billingApiUrl: process.env.BILLING_API_URL ?? '', + } +): BillingRepository { + return { + async createCheckout(tierId) { + const res = await fetch(`${deps.billingApiUrl}/checkouts`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + body: JSON.stringify({ + teamID: scope.teamId, + tierID: tierId, + }), + }) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + const data = (await res.json()) as { url: string; error?: string } + if (data.error) { + return err(repoErrorFromHttp(500, data.error)) + } + + return ok({ url: data.url }) + }, + async createCustomerPortalSession(origin) { + const res = await fetch(`${deps.billingApiUrl}/stripe/portal`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(origin ? { Origin: origin } : {}), + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + }) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as CustomerPortalResponse) + }, + async getItems() { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/items`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as TeamItems) + }, + async getUsage() { + const res = await fetch( + `${deps.billingApiUrl}/v2/teams/${scope.teamId}/usage`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + const responseData = (await res.json()) as UsageResponse + return ok({ + ...responseData, + hour_usages: responseData.hour_usages.map((hour) => ({ + ...hour, + timestamp: hour.timestamp * 1000, + })), + }) + }, + async getInvoices() { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/invoices`, + { + headers: { + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as Invoice[]) + }, + async getLimits() { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/billing-limits`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as BillingLimit) + }, + async setLimit(key, value) { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/billing-limits`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken), + }, + body: JSON.stringify({ + [key]: value, + }), + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok(undefined) + }, + async clearLimit(key) { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/billing-limits/${key}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok(undefined) + }, + async createOrder(itemId) { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/orders`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + body: JSON.stringify({ + items: [{ name: itemId, quantity: 1 }], + }), + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as AddOnOrderCreateResponse) + }, + async confirmOrder(orderId) { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/orders/${orderId}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as AddOnOrderConfirmResponse) + }, + async getCustomerSession() { + const res = await fetch( + `${deps.billingApiUrl}/teams/${scope.teamId}/payment-methods/customer-session`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...SUPABASE_AUTH_HEADERS(scope.accessToken, scope.teamId), + }, + } + ) + + if (!res.ok) { + return err(repoErrorFromHttp(res.status, await parseText(res))) + } + + return ok((await res.json()) as PaymentMethodsCustomerSession) + }, + } +} diff --git a/src/server/api/models/builds.models.ts b/src/core/domains/builds/models.ts similarity index 86% rename from src/server/api/models/builds.models.ts rename to src/core/domains/builds/models.ts index 9d5d04e59..032d4e1cf 100644 --- a/src/server/api/models/builds.models.ts +++ b/src/core/domains/builds/models.ts @@ -22,7 +22,7 @@ type _BuildStatusExhaustiveCheck = AssertTrue< // TypeCheck: End export const BuildStatusSchema = z.enum(BUILD_STATUS_VALUES) -export interface ListedBuildDTO { +export interface ListedBuildModel { id: string // id or alias template: string @@ -33,25 +33,25 @@ export interface ListedBuildDTO { finishedAt: number | null } -export interface RunningBuildStatusDTO { +export interface RunningBuildStatusModel { id: string status: BuildStatus finishedAt: number | null statusMessage: string | null } -export interface BuildLogDTO { +export interface BuildLogModel { timestampUnix: number level: infraComponents['schemas']['LogLevel'] message: string } -export interface BuildLogsDTO { - logs: BuildLogDTO[] +export interface BuildLogsModel { + logs: BuildLogModel[] nextCursor: number | null } -export interface BuildDetailsDTO { +export interface BuildDetailsModel { templateNames: string[] | null // id or alias template: string diff --git a/src/core/domains/builds/repository.server.ts b/src/core/domains/builds/repository.server.ts new file mode 100644 index 000000000..91c557d24 --- /dev/null +++ b/src/core/domains/builds/repository.server.ts @@ -0,0 +1,284 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { + BuildStatus, + ListedBuildModel, + RunningBuildStatusModel, +} from '@/core/domains/builds/models' +import { + handleDashboardApiError, + handleInfraApiError, +} from '@/core/server/adapters/trpc-errors' +import { INITIAL_BUILD_STATUSES } from '@/features/dashboard/templates/builds/constants' +import { api, infra } from '@/lib/clients/api' +import type { components as InfraComponents } from '@/types/infra-api.types' + +type BuildsRepositoryDeps = { + apiClient: typeof api + infraClient: typeof infra + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export interface BuildsScope { + accessToken: string + teamId: string +} + +export interface BuildsRepository { + listBuilds( + buildIdOrTemplate?: string, + statuses?: BuildStatus[], + options?: ListBuildsOptions + ): Promise + getRunningStatuses(buildIds: string[]): Promise + getBuildInfo(buildId: string): Promise + getInfraBuildStatus( + templateId: string, + buildId: string + ): Promise + getInfraBuildLogs( + templateId: string, + buildId: string, + options?: GetInfraBuildLogsOptions + ): Promise +} + +const LIST_BUILDS_DEFAULT_LIMIT = 50 +const LIST_BUILDS_MIN_LIMIT = 1 +const LIST_BUILDS_MAX_LIMIT = 100 + +function normalizeListBuildsLimit(limit?: number): number { + return Math.max( + LIST_BUILDS_MIN_LIMIT, + Math.min(limit ?? LIST_BUILDS_DEFAULT_LIMIT, LIST_BUILDS_MAX_LIMIT) + ) +} + +interface ListBuildsOptions { + limit?: number + cursor?: string +} + +interface ListBuildsResult { + data: ListedBuildModel[] + nextCursor: string | null +} + +interface BuildInfoResult { + names: string[] | null + createdAt: number + finishedAt: number | null + status: ListedBuildModel['status'] + statusMessage: string | null +} + +export interface GetInfraBuildLogsOptions { + cursor?: number + limit?: number + direction?: 'forward' | 'backward' + level?: 'debug' | 'info' | 'warn' | 'error' +} + +export function createBuildsRepository( + scope: BuildsScope, + deps: BuildsRepositoryDeps = { + apiClient: api, + infraClient: infra, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): BuildsRepository { + return { + async listBuilds( + buildIdOrTemplate, + statuses = INITIAL_BUILD_STATUSES, + options = {} + ): Promise { + const limit = normalizeListBuildsLimit(options.limit) + const result = await deps.apiClient.GET('/builds', { + params: { + query: { + build_id_or_template: buildIdOrTemplate?.trim() || undefined, + statuses, + limit, + cursor: options.cursor, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!result.response.ok || result.error) { + handleDashboardApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/builds', + logKey: 'repositories:builds:list_builds:dashboard_api_error', + }) + } + + const builds = result.data?.data ?? [] + if (builds.length === 0) { + return { + data: [], + nextCursor: null, + } + } + + return { + data: builds.map( + (build): ListedBuildModel => ({ + id: build.id, + template: build.template, + templateId: build.templateId, + status: build.status, + statusMessage: build.statusMessage, + createdAt: new Date(build.createdAt).getTime(), + finishedAt: build.finishedAt + ? new Date(build.finishedAt).getTime() + : null, + }) + ), + nextCursor: result.data?.nextCursor ?? null, + } + }, + async getRunningStatuses(buildIds) { + if (buildIds.length === 0) { + return [] + } + + const result = await deps.apiClient.GET('/builds/statuses', { + params: { + query: { + build_ids: buildIds, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!result.response.ok || result.error) { + handleDashboardApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/builds/statuses', + logKey: + 'repositories:builds:get_running_statuses:dashboard_api_error', + }) + } + + return (result.data?.buildStatuses ?? []).map((row) => ({ + id: row.id, + status: row.status, + finishedAt: row.finishedAt ? new Date(row.finishedAt).getTime() : null, + statusMessage: row.statusMessage, + })) + }, + async getBuildInfo(buildId) { + const result = await deps.apiClient.GET('/builds/{build_id}', { + params: { + path: { + build_id: buildId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!result.response.ok || result.error) { + handleDashboardApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/builds/{build_id}', + logKey: 'repositories:builds:get_build_info:dashboard_api_error', + context: { + build_id: buildId, + }, + }) + } + + const data = result.data + + return { + names: data.names ?? null, + createdAt: new Date(data.createdAt).getTime(), + finishedAt: data.finishedAt + ? new Date(data.finishedAt).getTime() + : null, + status: data.status, + statusMessage: data.statusMessage, + } + }, + async getInfraBuildStatus(templateId, buildId) { + const result = await deps.infraClient.GET( + '/templates/{templateID}/builds/{buildID}/status', + { + params: { + path: { + templateID: templateId, + buildID: buildId, + }, + query: { + limit: 0, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + } + ) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/templates/{templateID}/builds/{buildID}/status', + logKey: 'repositories:builds:get_build_status:infra_error', + }) + } + + return result.data + }, + async getInfraBuildLogs(templateId, buildId, options = {}) { + const result = await deps.infraClient.GET( + '/templates/{templateID}/builds/{buildID}/logs', + { + params: { + path: { + templateID: templateId, + buildID: buildId, + }, + query: { + cursor: options.cursor, + limit: options.limit, + direction: options.direction, + level: options.level, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + } + ) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/templates/{templateID}/builds/{buildID}/logs', + logKey: 'repositories:builds:get_build_logs:infra_error', + }) + } + + return result.data + }, + } +} diff --git a/src/core/domains/keys/repository.server.ts b/src/core/domains/keys/repository.server.ts new file mode 100644 index 000000000..5e014a179 --- /dev/null +++ b/src/core/domains/keys/repository.server.ts @@ -0,0 +1,99 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' +import { infra } from '@/lib/clients/api' +import type { CreatedTeamAPIKey, TeamAPIKey } from '@/types/api.types' + +type KeysRepositoryDeps = { + infraClient: typeof infra + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export interface KeysScope { + accessToken: string + teamId: string +} + +export interface KeysRepository { + listTeamApiKeys(): Promise> + createApiKey(name: string): Promise> + deleteApiKey(apiKeyId: string): Promise> +} + +export function createKeysRepository( + scope: KeysScope, + deps: KeysRepositoryDeps = { + infraClient: infra, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): KeysRepository { + return { + async listTeamApiKeys() { + const res = await deps.infraClient.GET('/api-keys', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to get API keys', + res.error + ) + ) + } + + return ok(res.data ?? []) + }, + async createApiKey(name) { + const res = await deps.infraClient.POST('/api-keys', { + body: { + name, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to create API key', + res.error + ) + ) + } + + return ok(res.data) + }, + async deleteApiKey(apiKeyId) { + const res = await deps.infraClient.DELETE('/api-keys/{apiKeyID}', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + params: { + path: { + apiKeyID: apiKeyId, + }, + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to delete API key', + res.error + ) + ) + } + + return ok(undefined) + }, + } +} diff --git a/src/server/api/models/sandboxes.models.ts b/src/core/domains/sandboxes/models.ts similarity index 82% rename from src/server/api/models/sandboxes.models.ts rename to src/core/domains/sandboxes/models.ts index 011b05a20..005f79fc4 100644 --- a/src/server/api/models/sandboxes.models.ts +++ b/src/core/domains/sandboxes/models.ts @@ -4,7 +4,7 @@ import type { components as InfraComponents } from '@/types/infra-api.types' export type SandboxLogLevel = InfraComponents['schemas']['LogLevel'] -interface SandboxDetailsBaseDTO { +interface SandboxDetailsBaseModel { templateID: string alias?: string sandboxID: string @@ -13,10 +13,10 @@ interface SandboxDetailsBaseDTO { cpuCount: number memoryMB: number diskSizeMB: number - lifecycle?: SandboxLifecycleDTO + lifecycle?: SandboxLifecycleModel } -interface ActiveSandboxDetailsDTO extends SandboxDetailsBaseDTO { +interface ActiveSandboxDetailsModel extends SandboxDetailsBaseModel { endAt: string envdVersion: string envdAccessToken?: string @@ -24,35 +24,35 @@ interface ActiveSandboxDetailsDTO extends SandboxDetailsBaseDTO { state: InfraComponents['schemas']['SandboxState'] } -interface KilledSandboxDetailsDTO extends SandboxDetailsBaseDTO { +interface KilledSandboxDetailsModel extends SandboxDetailsBaseModel { endAt: string | null stoppedAt: string | null state: 'killed' } -export type SandboxDetailsDTO = - | ActiveSandboxDetailsDTO - | KilledSandboxDetailsDTO +export type SandboxDetailsModel = + | ActiveSandboxDetailsModel + | KilledSandboxDetailsModel -export interface SandboxLogDTO { +export interface SandboxLogModel { timestampUnix: number level: SandboxLogLevel message: string } -export interface SandboxLogsDTO { - logs: SandboxLogDTO[] +export interface SandboxLogsModel { + logs: SandboxLogModel[] nextCursor: number | null } export type SandboxMetric = InfraComponents['schemas']['SandboxMetric'] -export type SandboxEventDTO = ArgusComponents['schemas']['SandboxEvent'] +export type SandboxEventModel = ArgusComponents['schemas']['SandboxEvent'] -export interface SandboxLifecycleDTO { +export interface SandboxLifecycleModel { createdAt: string | null pausedAt: string | null endedAt: string | null - events: SandboxEventDTO[] + events: SandboxEventModel[] } const SANDBOX_LIFECYCLE_EVENT_PREFIX = 'sandbox.lifecycle.' @@ -74,7 +74,9 @@ function parseEventTimestampMs(value: string): number | null { return timestampMs } -function sortEventsByTimestamp(events: SandboxEventDTO[]): SandboxEventDTO[] { +function sortEventsByTimestamp( + events: SandboxEventModel[] +): SandboxEventModel[] { return [...events].sort((a, b) => { const timestampA = parseEventTimestampMs(a.timestamp) ?? Number.MAX_SAFE_INTEGER @@ -89,8 +91,8 @@ function sortEventsByTimestamp(events: SandboxEventDTO[]): SandboxEventDTO[] { } export function deriveSandboxLifecycleFromEvents( - events: SandboxEventDTO[] -): SandboxLifecycleDTO { + events: SandboxEventModel[] +): SandboxLifecycleModel { const lifecycleEvents = sortEventsByTimestamp( events.filter((event) => event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX) @@ -140,9 +142,9 @@ export function deriveSandboxLifecycleFromEvents( // mappings -export function mapInfraSandboxLogToDTO( +export function mapInfraSandboxLogToModel( log: InfraComponents['schemas']['SandboxLogEntry'] -): SandboxLogDTO { +): SandboxLogModel { return { timestampUnix: new Date(log.timestamp).getTime(), level: log.level, @@ -150,9 +152,9 @@ export function mapInfraSandboxLogToDTO( } } -export function mapInfraSandboxDetailsToDTO( +export function mapInfraSandboxDetailsToModel( sandbox: InfraComponents['schemas']['SandboxDetail'] -): SandboxDetailsDTO { +): SandboxDetailsModel { return { templateID: sandbox.templateID, alias: sandbox.alias, @@ -170,9 +172,9 @@ export function mapInfraSandboxDetailsToDTO( } } -export function mapApiSandboxRecordToDTO( +export function mapApiSandboxRecordToModel( sandbox: DashboardComponents['schemas']['SandboxRecord'] -): KilledSandboxDetailsDTO { +): KilledSandboxDetailsModel { const stoppedAt = sandbox.stoppedAt ?? null return { diff --git a/src/core/domains/sandboxes/repository.server.ts b/src/core/domains/sandboxes/repository.server.ts new file mode 100644 index 000000000..af9dd4200 --- /dev/null +++ b/src/core/domains/sandboxes/repository.server.ts @@ -0,0 +1,459 @@ +import 'server-only' + +import { TRPCError } from '@trpc/server' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { SandboxEventModel } from '@/core/domains/sandboxes/models' +import { + apiError, + handleDashboardApiError, + handleInfraApiError, +} from '@/core/server/adapters/trpc-errors' +import { api, infra } from '@/lib/clients/api' +import { l } from '@/lib/clients/logger/logger' +import type { + Sandboxes, + SandboxesMetricsRecord, + TeamMetric, +} from '@/types/api.types' +import type { components as DashboardComponents } from '@/types/dashboard-api.types' +import type { components as InfraComponents } from '@/types/infra-api.types' + +type SandboxesRepositoryDeps = { + apiClient: typeof api + infraClient: typeof infra + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export interface SandboxesRequestScope { + accessToken: string + teamId: string +} + +export interface GetSandboxLogsOptions { + cursor?: number + limit?: number + direction?: 'forward' | 'backward' + level?: 'debug' | 'info' | 'warn' | 'error' + search?: string +} + +export interface GetSandboxMetricsOptions { + startUnixMs: number + endUnixMs: number +} + +export interface SandboxesRepository { + getSandboxLogs( + sandboxId: string, + options?: GetSandboxLogsOptions + ): Promise + getSandboxDetails(sandboxId: string): Promise< + | { + source: 'infra' + details: InfraComponents['schemas']['SandboxDetail'] + } + | { + source: 'database-record' + details: DashboardComponents['schemas']['SandboxRecord'] + } + > + getSandboxLifecycleEvents(sandboxId: string): Promise + getSandboxMetrics( + sandboxId: string, + options: GetSandboxMetricsOptions + ): Promise + listSandboxes(): Promise + getSandboxesMetrics(sandboxIds: string[]): Promise + getTeamMetricsRange( + startUnixSeconds: number, + endUnixSeconds: number + ): Promise + getTeamMetricsMax( + startUnixSeconds: number, + endUnixSeconds: number, + metric: 'concurrent_sandboxes' | 'sandbox_start_rate' + ): Promise +} + +const SANDBOX_EVENTS_PAGE_SIZE = 100 +const SANDBOX_EVENTS_MAX_PAGES = 50 +const SANDBOX_LIFECYCLE_EVENT_PREFIX = 'sandbox.lifecycle.' + +export function createSandboxesRepository( + scope: SandboxesRequestScope, + deps: SandboxesRepositoryDeps = { + apiClient: api, + infraClient: infra, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): SandboxesRepository { + return { + async getSandboxLogs(sandboxId, options = {}) { + const result = await deps.infraClient.GET( + '/v2/sandboxes/{sandboxID}/logs', + { + params: { + path: { + sandboxID: sandboxId, + }, + query: { + cursor: options.cursor, + limit: options.limit, + direction: options.direction, + level: options.level, + search: options.search, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + } + ) + + if (!result.response.ok || result.error) { + const status = result.response.status + + l.error( + { + key: 'repositories:sandboxes:get_sandbox_logs:infra_error', + error: result.error, + team_id: scope.teamId, + context: { + status, + path: '/v2/sandboxes/{sandboxID}/logs', + sandbox_id: sandboxId, + }, + }, + `failed to fetch /v2/sandboxes/{sandboxID}/logs: ${result.error?.message || 'Unknown error'}` + ) + + if (status === 404) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: "Sandbox not found or you don't have access to it", + }) + } + + throw apiError(status) + } + + return result.data + }, + async getSandboxDetails(sandboxId) { + const infraResult = await deps.infraClient.GET('/sandboxes/{sandboxID}', { + params: { + path: { + sandboxID: sandboxId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + }) + + if (infraResult.response.ok && infraResult.data) { + return { + source: 'infra' as const, + details: infraResult.data, + } + } + + const infraStatus = infraResult.response.status + + if (infraStatus !== 404) { + handleInfraApiError({ + status: infraStatus, + error: infraResult.error, + teamId: scope.teamId, + path: '/sandboxes/{sandboxID}', + logKey: 'repositories:sandboxes:get_sandbox_details:infra_error', + context: { + sandbox_id: sandboxId, + }, + }) + } + + const dashboardResult = await deps.apiClient.GET( + '/sandboxes/{sandboxID}/record', + { + params: { + path: { + sandboxID: sandboxId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + } + ) + + if (dashboardResult.response.ok && dashboardResult.data) { + return { + source: 'database-record' as const, + details: dashboardResult.data, + } + } + + const dashboardStatus = dashboardResult.response.status + + if (dashboardStatus === 404) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: "Sandbox not found or you don't have access to it", + }) + } + + handleDashboardApiError({ + status: dashboardStatus, + error: dashboardResult.error, + teamId: scope.teamId, + path: '/sandboxes/{sandboxID}/record', + logKey: 'repositories:sandboxes:get_sandbox_details:fallback_error', + context: { + infra_status: infraStatus, + sandbox_id: sandboxId, + }, + }) + }, + async getSandboxLifecycleEvents(sandboxId) { + const lifecycleEvents: SandboxEventModel[] = [] + + for ( + let pageIndex = 0, offset = 0; + pageIndex < SANDBOX_EVENTS_MAX_PAGES; + pageIndex += 1, offset += SANDBOX_EVENTS_PAGE_SIZE + ) { + try { + const result = await deps.infraClient.GET( + '/events/sandboxes/{sandboxID}', + { + params: { + path: { + sandboxID: sandboxId, + }, + query: { + offset, + limit: SANDBOX_EVENTS_PAGE_SIZE, + orderAsc: true, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + } + ) + + if (!result.response.ok || result.error) { + l.warn({ + key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_error', + error: result.error, + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/events/sandboxes/{sandboxID}', + sandbox_id: sandboxId, + offset, + limit: SANDBOX_EVENTS_PAGE_SIZE, + }, + }) + break + } + + const page = result.data ?? [] + lifecycleEvents.push( + ...page.filter((event) => + event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX) + ) + ) + + if (page.length < SANDBOX_EVENTS_PAGE_SIZE) { + break + } + } catch (error) { + l.warn({ + key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_exception', + error, + team_id: scope.teamId, + context: { + path: '/events/sandboxes/{sandboxID}', + sandbox_id: sandboxId, + offset, + limit: SANDBOX_EVENTS_PAGE_SIZE, + }, + }) + break + } + } + + return lifecycleEvents + }, + async getSandboxMetrics(sandboxId, options) { + const startUnixSeconds = Math.floor(options.startUnixMs / 1000) + const endUnixSeconds = Math.floor(options.endUnixMs / 1000) + + const result = await deps.infraClient.GET( + '/sandboxes/{sandboxID}/metrics', + { + params: { + path: { + sandboxID: sandboxId, + }, + query: { + start: startUnixSeconds, + end: endUnixSeconds, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + } + ) + + if (!result.response.ok || result.error) { + const status = result.response.status + + l.error( + { + key: 'repositories:sandboxes:get_sandbox_metrics:infra_error', + error: result.error, + team_id: scope.teamId, + context: { + status, + path: '/sandboxes/{sandboxID}/metrics', + sandbox_id: sandboxId, + }, + }, + `failed to fetch /sandboxes/{sandboxID}/metrics: ${result.error?.message || 'Unknown error'}` + ) + + if (status === 404) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: "Sandbox not found or you don't have access to it", + }) + } + + throw apiError(status) + } + + return result.data + }, + async listSandboxes() { + const result = await deps.infraClient.GET('/sandboxes', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + }) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/sandboxes', + logKey: 'repositories:sandboxes:list_sandboxes:infra_error', + }) + } + + return result.data + }, + async getSandboxesMetrics(sandboxIds) { + const result = await deps.infraClient.GET('/sandboxes/metrics', { + params: { + query: { + sandbox_ids: sandboxIds, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + }) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/sandboxes/metrics', + logKey: 'repositories:sandboxes:get_sandboxes_metrics:infra_error', + context: { sandbox_ids: sandboxIds }, + }) + } + + return result.data.sandboxes + }, + async getTeamMetricsRange(startUnixSeconds, endUnixSeconds) { + const result = await deps.infraClient.GET('/teams/{teamID}/metrics', { + params: { + path: { + teamID: scope.teamId, + }, + query: { + start: startUnixSeconds, + end: endUnixSeconds, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + }) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/teams/{teamID}/metrics', + logKey: 'repositories:sandboxes:get_team_metrics:infra_error', + context: { + start_unix_seconds: startUnixSeconds, + end_unix_seconds: endUnixSeconds, + }, + }) + } + + return result.data + }, + async getTeamMetricsMax(startUnixSeconds, endUnixSeconds, metric) { + const result = await deps.infraClient.GET('/teams/{teamID}/metrics/max', { + params: { + path: { + teamID: scope.teamId, + }, + query: { + start: startUnixSeconds, + end: endUnixSeconds, + metric, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + cache: 'no-store', + }) + + if (!result.response.ok || result.error) { + handleInfraApiError({ + status: result.response.status, + error: result.error, + teamId: scope.teamId, + path: '/teams/{teamID}/metrics/max', + logKey: 'repositories:sandboxes:get_team_metrics_max:infra_error', + context: { + start_unix_seconds: startUnixSeconds, + end_unix_seconds: endUnixSeconds, + metric, + }, + }) + } + + return result.data + }, + } +} diff --git a/src/server/api/schemas/sandboxes.ts b/src/core/domains/sandboxes/schemas.ts similarity index 100% rename from src/server/api/schemas/sandboxes.ts rename to src/core/domains/sandboxes/schemas.ts diff --git a/src/core/domains/support/repository.server.ts b/src/core/domains/support/repository.server.ts new file mode 100644 index 000000000..104434fea --- /dev/null +++ b/src/core/domains/support/repository.server.ts @@ -0,0 +1,249 @@ +import 'server-only' + +import { AttachmentType, PlainClient } from '@team-plain/typescript-sdk' +import { TRPCError } from '@trpc/server' +import { createTeamsRepository } from '@/core/domains/teams/repository.server' +import { l } from '@/lib/clients/logger/logger' + +const MAX_FILE_SIZE = 10 * 1024 * 1024 +const MAX_FILES = 5 + +interface FileInput { + name: string + type: string + base64: string +} + +type SupportRepositoryDeps = { + createPlainClient: () => PlainClient +} + +export interface SupportScope { + accessToken: string + teamId?: string +} + +export interface SupportRepository { + getTeamSupportData(): Promise<{ + name: string + email: string + tier: string + }> + createSupportThread(input: { + description: string + files?: FileInput[] + teamId: string + teamName: string + customerEmail: string + accountOwnerEmail: string + customerTier: string + }): Promise<{ threadId: string }> +} + +function formatThreadText(input: { + description: string + teamId: string + customerEmail: string + accountOwnerEmail: string + customerTier: string +}): string { + const { + description, + teamId, + customerEmail, + accountOwnerEmail, + customerTier, + } = input + + const header = [ + '########', + `Customer: ${customerEmail}`, + `Account Owner: ${accountOwnerEmail}`, + `Tier: ${customerTier}`, + `TeamID: ${teamId}`, + `Orbit: https://orbit.e2b.dev/teams/${teamId}/users`, + '########', + ].join('\n') + + const truncatedDescription = description.slice(0, 10000) + + return `${header}\n\n${truncatedDescription}` +} + +async function uploadAttachmentToPlain( + client: PlainClient, + customerId: string, + file: FileInput +): Promise { + const buffer = Buffer.from(file.base64, 'base64') + + if (buffer.byteLength > MAX_FILE_SIZE) { + throw new Error(`File ${file.name} exceeds 10MB limit`) + } + + const uploadUrlResult = await client.createAttachmentUploadUrl({ + customerId, + fileName: file.name, + fileSizeBytes: buffer.byteLength, + attachmentType: AttachmentType.CustomTimelineEntry, + }) + + if (uploadUrlResult.error) { + throw new Error( + `Failed to create upload URL for ${file.name}: ${uploadUrlResult.error.message}` + ) + } + + const { uploadFormUrl, uploadFormData, attachment } = uploadUrlResult.data + const formData = new FormData() + for (const { key, value } of uploadFormData) { + formData.append(key, value) + } + formData.append('file', new Blob([buffer], { type: file.type }), file.name) + + const uploadResponse = await fetch(uploadFormUrl, { + method: 'POST', + body: formData, + }) + + if (!uploadResponse.ok) { + throw new Error( + `Failed to upload ${file.name}: ${uploadResponse.status} ${uploadResponse.statusText}` + ) + } + + return attachment.id +} + +export function createSupportRepository( + scope: SupportScope, + deps: SupportRepositoryDeps = { + createPlainClient: () => + new PlainClient({ + apiKey: process.env.PLAIN_API_KEY ?? '', + }), + } +): SupportRepository { + const requireTeamId = (teamId?: string): string => { + if (!teamId) { + throw new Error('teamId is required in request scope') + } + return teamId + } + + return { + async getTeamSupportData() { + const teamResult = await createTeamsRepository({ + accessToken: scope.accessToken, + teamId: requireTeamId(scope.teamId), + }).getCurrentUserTeam(requireTeamId(scope.teamId)) + + if (!teamResult.ok) { + l.error( + { + key: 'repositories:support:fetch_team_error', + error: teamResult.error, + team_id: scope.teamId, + }, + 'failed to fetch team data' + ) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to load team information', + }) + } + + const team = teamResult.data + + return { name: team.name, email: team.email, tier: team.tier } + }, + async createSupportThread(input) { + if (!process.env.PLAIN_API_KEY) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Support API not configured', + }) + } + + const { + description, + files, + teamId, + teamName, + customerEmail, + accountOwnerEmail, + customerTier, + } = input + + const client = deps.createPlainClient() + const customerResult = await client.upsertCustomer({ + identifier: { + emailAddress: customerEmail, + }, + onCreate: { + email: { + email: customerEmail, + isVerified: true, + }, + fullName: customerEmail, + }, + onUpdate: {}, + }) + + if (customerResult.error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to create support ticket', + }) + } + + const customerId = customerResult.data.customer.id + const attachmentIds: string[] = [] + const validFiles = (files ?? []).slice(0, MAX_FILES) + + for (const file of validFiles) { + try { + const attachmentId = await uploadAttachmentToPlain( + client, + customerId, + file + ) + attachmentIds.push(attachmentId) + } catch {} + } + + const title = `Support Request [${teamName}]` + const threadText = formatThreadText({ + description, + teamId, + customerEmail, + accountOwnerEmail, + customerTier, + }) + + const result = await client.createThread({ + title, + customerIdentifier: { + customerId, + }, + components: [ + { + componentText: { + text: threadText, + }, + }, + ], + ...(attachmentIds.length > 0 ? { attachmentIds } : {}), + }) + + if (result.error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to create support ticket', + }) + } + + return { threadId: result.data.id } + }, + } +} diff --git a/src/core/domains/teams/models.ts b/src/core/domains/teams/models.ts new file mode 100644 index 000000000..0d50f07fe --- /dev/null +++ b/src/core/domains/teams/models.ts @@ -0,0 +1,31 @@ +export type { ClientTeam } from '@/types/dashboard.types' + +export type TeamLimits = { + concurrentInstances: number + diskMb: number + maxLengthHours: number + maxRamMb: number + maxVcpu: number +} + +export type TeamMemberInfo = { + id: string + email: string + name?: string + avatar_url?: string +} + +export type TeamMemberRelation = { + added_by: string | null + is_default: boolean +} + +export type TeamMember = { + info: TeamMemberInfo + relation: TeamMemberRelation +} + +export type ResolvedTeam = { + id: string + slug: string +} diff --git a/src/core/domains/teams/repository.server.ts b/src/core/domains/teams/repository.server.ts new file mode 100644 index 000000000..7843bc083 --- /dev/null +++ b/src/core/domains/teams/repository.server.ts @@ -0,0 +1,333 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' +import { api } from '@/lib/clients/api' +import { supabaseAdmin } from '@/lib/clients/supabase/admin' +import type { components as DashboardComponents } from '@/types/dashboard-api.types' +import type { ClientTeam, ResolvedTeam, TeamLimits, TeamMember } from './models' + +type ApiUserTeam = { + id: string + name: string + slug: string + tier: string + email: string + isDefault: boolean + limits: { + concurrentSandboxes: number + diskMb: number + maxLengthHours: number + maxRamMb: number + maxVcpu: number + } +} + +function mapApiTeamToClientTeam(apiTeam: ApiUserTeam): ClientTeam { + return { + id: apiTeam.id, + name: apiTeam.name, + slug: apiTeam.slug, + tier: apiTeam.tier, + email: apiTeam.email, + is_default: apiTeam.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + } +} + +type TeamsRepositoryDeps = { + apiClient: typeof api + authHeaders: typeof SUPABASE_AUTH_HEADERS + adminClient: typeof supabaseAdmin +} + +export interface TeamsRequestScope { + accessToken: string + teamId?: string +} + +export interface TeamsRepository { + listUserTeams(): Promise> + getCurrentUserTeam(teamIdOrSlug: string): Promise> + resolveTeamBySlug( + slug: string, + next?: { tags?: string[] } + ): Promise> + getTeamLimitsByIdOrSlug(teamIdOrSlug: string): Promise> + listTeamMembers(): Promise> + updateTeamName( + name: string + ): Promise> + addTeamMember(email: string): Promise> + removeTeamMember(userId: string): Promise> + updateTeamProfilePictureUrl( + profilePictureUrl: string + ): Promise> +} + +export function createTeamsRepository( + scope: TeamsRequestScope, + deps: TeamsRepositoryDeps = { + apiClient: api, + authHeaders: SUPABASE_AUTH_HEADERS, + adminClient: supabaseAdmin, + } +): TeamsRepository { + const requireTeamId = (teamId?: string): string => { + if (!teamId) { + throw new Error('teamId is required in request scope') + } + return teamId + } + + const listApiUserTeams = async ( + accessToken: string + ): Promise> => { + const { data, error, response } = await deps.apiClient.GET('/teams', { + headers: deps.authHeaders(accessToken), + }) + + if (!response.ok || error || !data?.teams) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to fetch user teams', + error + ) + ) + } + + return ok(data.teams as ApiUserTeam[]) + } + + return { + async listUserTeams(): Promise> { + const teamsResult = await listApiUserTeams(scope.accessToken) + + if (!teamsResult.ok) { + return teamsResult + } + + return ok(teamsResult.data.map(mapApiTeamToClientTeam)) + }, + async getCurrentUserTeam( + teamIdOrSlug: string + ): Promise> { + const teamsResult = await listApiUserTeams(scope.accessToken) + + if (!teamsResult.ok) { + return teamsResult + } + + const team = teamsResult.data.find( + (candidate) => + candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug + ) + + if (!team) { + return err( + repoErrorFromHttp(403, 'Team not found or access denied', { + teamIdOrSlug, + }) + ) + } + + return ok(mapApiTeamToClientTeam(team)) + }, + async resolveTeamBySlug( + slug: string, + next?: { tags?: string[] } + ): Promise> { + const { data, error, response } = await deps.apiClient.GET( + '/teams/resolve', + { + params: { query: { slug } }, + headers: deps.authHeaders(scope.accessToken), + next, + } + ) + + if (!response.ok || error || !data) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to resolve team', + error + ) + ) + } + + return ok({ + id: data.id, + slug: data.slug, + }) + }, + async getTeamLimitsByIdOrSlug( + teamIdOrSlug: string + ): Promise> { + const teamsResult = await listApiUserTeams(scope.accessToken) + + if (!teamsResult.ok) { + return teamsResult + } + + const team = teamsResult.data.find( + (candidate) => + candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug + ) + + if (!team) { + return err(repoErrorFromHttp(404, 'Team not found')) + } + + return ok({ + concurrentInstances: team.limits.concurrentSandboxes, + diskMb: team.limits.diskMb, + maxLengthHours: team.limits.maxLengthHours, + maxRamMb: team.limits.maxRamMb, + maxVcpu: team.limits.maxVcpu, + }) + }, + async listTeamMembers(): Promise> { + const teamId = requireTeamId(scope.teamId) + const { data, error, response } = await deps.apiClient.GET( + '/teams/{teamId}/members', + { + params: { path: { teamId } }, + headers: deps.authHeaders(scope.accessToken, teamId), + } + ) + + if (!response.ok || error) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to fetch team members', + error + ) + ) + } + + const members = data?.members ?? [] + const enrichedMembers = await Promise.all( + members.map(async (member) => { + const { data: userData } = + await deps.adminClient.auth.admin.getUserById(member.id) + const user = userData.user + + return { + info: { + id: member.id, + email: member.email, + name: user?.user_metadata?.name, + avatar_url: user?.user_metadata?.avatar_url, + }, + relation: { + added_by: member.addedBy ?? null, + is_default: member.isDefault, + }, + } satisfies TeamMember + }) + ) + + return ok(enrichedMembers) + }, + async updateTeamName( + name + ): Promise< + RepoResult + > { + const teamId = requireTeamId(scope.teamId) + const { data, error, response } = await deps.apiClient.PATCH( + '/teams/{teamId}', + { + params: { path: { teamId } }, + headers: deps.authHeaders(scope.accessToken, teamId), + body: { name }, + } + ) + + if (!response.ok || error || !data) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to update team name', + error + ) + ) + } + + return ok(data) + }, + async addTeamMember(email): Promise> { + const teamId = requireTeamId(scope.teamId) + const { error, response } = await deps.apiClient.POST( + '/teams/{teamId}/members', + { + params: { path: { teamId } }, + headers: deps.authHeaders(scope.accessToken, teamId), + body: { email }, + } + ) + + if (!response.ok || error) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to add team member', + error + ) + ) + } + + return ok(undefined) + }, + async removeTeamMember(userId): Promise> { + const teamId = requireTeamId(scope.teamId) + const { error, response } = await deps.apiClient.DELETE( + '/teams/{teamId}/members/{userId}', + { + params: { path: { teamId, userId } }, + headers: deps.authHeaders(scope.accessToken, teamId), + } + ) + + if (!response.ok || error) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to remove team member', + error + ) + ) + } + + return ok(undefined) + }, + async updateTeamProfilePictureUrl( + profilePictureUrl + ): Promise> { + const teamId = requireTeamId(scope.teamId) + const { data, error } = await deps.adminClient + .from('teams') + .update({ profile_picture_url: profilePictureUrl }) + .eq('id', teamId) + .select() + .single() + + if (error || !data) { + return err( + repoErrorFromHttp(500, error?.message ?? 'Failed to update team') + ) + } + + return ok(data as ClientTeam) + }, + } +} diff --git a/src/core/domains/teams/schemas.ts b/src/core/domains/teams/schemas.ts new file mode 100644 index 000000000..3da295d6c --- /dev/null +++ b/src/core/domains/teams/schemas.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' + +export { TeamIdOrSlugSchema } + +export const TeamNameSchema = z + .string() + .trim() + .min(1, { message: 'Team name cannot be empty' }) + .max(32, { message: 'Team name cannot be longer than 32 characters' }) + .regex(/^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$/, { + message: + 'Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots', + }) + +export const UpdateTeamNameSchema = z.object({ + teamIdOrSlug: TeamIdOrSlugSchema, + name: TeamNameSchema, +}) + +export const CreateTeamSchema = z.object({ + name: TeamNameSchema, +}) diff --git a/src/core/domains/templates/repository.server.ts b/src/core/domains/templates/repository.server.ts new file mode 100644 index 000000000..9fae7bf4e --- /dev/null +++ b/src/core/domains/templates/repository.server.ts @@ -0,0 +1,193 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { CACHE_TAGS } from '@/configs/cache' +import { USE_MOCK_DATA } from '@/configs/flags' +import { + MOCK_DEFAULT_TEMPLATES_DATA, + MOCK_TEMPLATES_DATA, +} from '@/configs/mock-data' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' +import { api, infra } from '@/lib/clients/api' +import type { DefaultTemplate, Template } from '@/types/api.types' + +type TemplatesRepositoryDeps = { + apiClient: typeof api + infraClient: typeof infra + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export interface TemplatesScope { + accessToken: string + teamId?: string +} + +export interface TemplatesRepository { + getTeamTemplates(): Promise> + getDefaultTemplatesCached(): Promise< + RepoResult<{ templates: DefaultTemplate[] }> + > + deleteTemplate(templateId: string): Promise> + updateTemplateVisibility( + templateId: string, + isPublic: boolean + ): Promise> +} + +export function createTemplatesRepository( + scope: TemplatesScope, + deps: TemplatesRepositoryDeps = { + apiClient: api, + infraClient: infra, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): TemplatesRepository { + const requireTeamId = (teamId?: string): string => { + if (!teamId) { + throw new Error('teamId is required in request scope') + } + return teamId + } + + return { + async getTeamTemplates() { + if (USE_MOCK_DATA) { + return ok({ + templates: MOCK_TEMPLATES_DATA, + }) + } + + const res = await deps.infraClient.GET('/templates', { + params: { + query: { + teamID: requireTeamId(scope.teamId), + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to fetch templates', + res.error + ) + ) + } + + return ok({ + templates: res.data, + }) + }, + async getDefaultTemplatesCached() { + if (USE_MOCK_DATA) { + return ok({ + templates: MOCK_DEFAULT_TEMPLATES_DATA, + }) + } + + const { data, error, response } = await deps.apiClient.GET( + '/templates/defaults', + { + headers: deps.authHeaders(scope.accessToken), + next: { tags: [CACHE_TAGS.DEFAULT_TEMPLATES] }, + } + ) + + if (!response.ok || error) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to fetch default templates', + error + ) + ) + } + + if (!data?.templates || data.templates.length === 0) { + return ok({ templates: [] }) + } + + const templates: DefaultTemplate[] = data.templates.map((t) => ({ + templateID: t.id, + buildID: t.buildId, + cpuCount: t.vcpu, + memoryMB: t.ramMb, + diskSizeMB: t.totalDiskSizeMb ?? 0, + envdVersion: t.envdVersion ?? '', + public: t.public, + aliases: t.aliases.map((a) => a.alias), + names: t.aliases.map((a) => { + if (a.namespace && a.namespace.length > 0) { + return `${a.namespace}/${a.alias}` + } + return a.alias + }), + createdAt: t.createdAt, + updatedAt: t.createdAt, + createdBy: null, + lastSpawnedAt: t.createdAt, + spawnCount: t.spawnCount, + buildCount: t.buildCount, + isDefault: true as const, + })) + + return ok({ templates }) + }, + async deleteTemplate(templateId) { + const res = await deps.infraClient.DELETE('/templates/{templateID}', { + params: { + path: { + templateID: templateId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to delete template', + res.error + ) + ) + } + + return ok({ success: true as const }) + }, + async updateTemplateVisibility(templateId, isPublic) { + const res = await deps.infraClient.PATCH('/templates/{templateID}', { + body: { + public: isPublic, + }, + params: { + path: { + templateID: templateId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to update template', + res.error + ) + ) + } + + return ok({ success: true as const, public: isPublic }) + }, + } +} diff --git a/src/core/domains/webhooks/repository.server.ts b/src/core/domains/webhooks/repository.server.ts new file mode 100644 index 000000000..2aab48b72 --- /dev/null +++ b/src/core/domains/webhooks/repository.server.ts @@ -0,0 +1,168 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' +import { infra } from '@/lib/clients/api' +import type { components as ArgusComponents } from '@/types/argus-api.types' + +type WebhooksRepositoryDeps = { + infraClient: typeof infra + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export interface WebhooksScope { + accessToken: string + teamId: string +} + +export interface UpsertWebhookInput { + mode: 'create' | 'edit' + webhookId?: string + name: string + url: string + events: string[] + signatureSecret?: string + enabled: boolean +} + +export interface WebhooksRepository { + listWebhooks(): Promise< + RepoResult + > + upsertWebhook(input: UpsertWebhookInput): Promise> + deleteWebhook(webhookId: string): Promise> + updateWebhookSecret( + webhookId: string, + signatureSecret: string + ): Promise> +} + +export function createWebhooksRepository( + scope: WebhooksScope, + deps: WebhooksRepositoryDeps = { + infraClient: infra, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): WebhooksRepository { + return { + async listWebhooks() { + const response = await deps.infraClient.GET('/events/webhooks', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!response.response.ok || response.error) { + if (response.response.status === 404) { + return ok([]) + } + + return err( + repoErrorFromHttp( + response.response.status, + response.error?.message ?? 'Failed to list webhooks', + response.error + ) + ) + } + + return ok(response.data ?? []) + }, + async upsertWebhook(input) { + const response = + input.mode === 'edit' + ? await deps.infraClient.PATCH('/events/webhooks/{webhookID}', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + params: { + path: { webhookID: input.webhookId ?? '' }, + }, + body: { + name: input.name, + url: input.url, + events: input.events, + enabled: input.enabled, + }, + }) + : await deps.infraClient.POST('/events/webhooks', { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + body: { + name: input.name, + url: input.url, + events: input.events, + enabled: input.enabled, + signatureSecret: input.signatureSecret ?? '', + }, + }) + + if (!response.response.ok || response.error) { + return err( + repoErrorFromHttp( + response.response.status, + response.error?.message ?? 'Failed to upsert webhook', + response.error + ) + ) + } + + return ok(undefined) + }, + async deleteWebhook(webhookId) { + const response = await deps.infraClient.DELETE( + '/events/webhooks/{webhookID}', + { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + params: { + path: { webhookID: webhookId }, + }, + } + ) + + if (!response.response.ok || response.error) { + return err( + repoErrorFromHttp( + response.response.status, + response.error?.message ?? 'Failed to delete webhook', + response.error + ) + ) + } + + return ok(undefined) + }, + async updateWebhookSecret(webhookId, signatureSecret) { + const response = await deps.infraClient.PATCH( + '/events/webhooks/{webhookID}', + { + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + params: { + path: { webhookID: webhookId }, + }, + body: { + signatureSecret, + }, + } + ) + + if (!response.response.ok || response.error) { + return err( + repoErrorFromHttp( + response.response.status, + response.error?.message ?? 'Failed to update webhook secret', + response.error + ) + ) + } + + return ok(undefined) + }, + } +} diff --git a/src/server/auth/auth-actions.ts b/src/core/server/actions/auth-actions.ts similarity index 98% rename from src/server/auth/auth-actions.ts rename to src/core/server/actions/auth-actions.ts index ed00d4e10..79407b206 100644 --- a/src/server/auth/auth-actions.ts +++ b/src/core/server/actions/auth-actions.ts @@ -7,6 +7,15 @@ import { z } from 'zod' import { CAPTCHA_REQUIRED_SERVER } from '@/configs/flags' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { USER_MESSAGES } from '@/configs/user-messages' +import { + forgotPasswordSchema, + signInSchema, + signUpSchema, +} from '@/core/server/functions/auth/auth.types' +import { + shouldWarnAboutAlternateEmail, + validateEmail, +} from '@/core/server/functions/auth/validate-email' import { verifyTurnstileToken } from '@/lib/captcha/turnstile' import { actionClient } from '@/lib/clients/action' import { l } from '@/lib/clients/logger/logger' @@ -14,11 +23,6 @@ import { createClient } from '@/lib/clients/supabase/server' import { relativeUrlSchema } from '@/lib/schemas/url' import { returnServerError } from '@/lib/utils/action' import { encodedRedirect } from '@/lib/utils/auth' -import { - shouldWarnAboutAlternateEmail, - validateEmail, -} from '@/server/auth/validate-email' -import { forgotPasswordSchema, signInSchema, signUpSchema } from './auth.types' async function validateCaptcha(captchaToken: string | undefined) { if (!CAPTCHA_REQUIRED_SERVER) { diff --git a/src/server/keys/key-actions.ts b/src/core/server/actions/key-actions.ts similarity index 65% rename from src/server/keys/key-actions.ts rename to src/core/server/actions/key-actions.ts index d5c410feb..606e7a99c 100644 --- a/src/server/keys/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -2,10 +2,8 @@ import { revalidatePath, updateTag } from 'next/cache' import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' @@ -26,27 +24,17 @@ export const createApiKeyAction = authActionClient .metadata({ actionName: 'createApiKey' }) .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { - const { session, teamId } = ctx const { name } = parsedInput - const accessToken = session.access_token + const result = await ctx.services.keys.createApiKey(name) - const res = await infra.POST('/api-keys', { - body: { - name, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (res.error) { + if (!result.ok) { l.error({ key: 'create_api_key:error', - message: res.error.message, - error: res.error, - team_id: teamId, - user_id: session.user.id, + message: result.error.message, + error: result.error, + team_id: ctx.teamId, + user_id: ctx.session.user.id, context: { name, }, @@ -59,7 +47,7 @@ export const createApiKeyAction = authActionClient revalidatePath(`/dashboard/${parsedInput.teamIdOrSlug}/keys`, 'page') return { - createdApiKey: res.data, + createdApiKey: result.data, } }) @@ -76,28 +64,15 @@ export const deleteApiKeyAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { apiKeyId } = parsedInput - const { session, teamId } = ctx - - const accessToken = session.access_token - - const res = await infra.DELETE('/api-keys/{apiKeyID}', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - params: { - path: { - apiKeyID: apiKeyId, - }, - }, - }) + const result = await ctx.services.keys.deleteApiKey(apiKeyId) - if (res.error) { + if (!result.ok) { l.error({ key: 'delete_api_key_action:error', - message: res.error.message, - error: res.error, - team_id: teamId, - user_id: session.user.id, + message: result.error.message, + error: result.error, + team_id: ctx.teamId, + user_id: ctx.session.user.id, context: { apiKeyId, }, diff --git a/src/server/sandboxes/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts similarity index 100% rename from src/server/sandboxes/sandbox-actions.ts rename to src/core/server/actions/sandbox-actions.ts diff --git a/src/server/team/team-actions.ts b/src/core/server/actions/team-actions.ts similarity index 76% rename from src/server/team/team-actions.ts rename to src/core/server/actions/team-actions.ts index 5dbd5a1e3..1023b881b 100644 --- a/src/server/team/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -8,14 +8,16 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { zfd } from 'zod-form-data' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { + CreateTeamSchema, + UpdateTeamNameSchema, +} from '@/core/domains/teams/schemas' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { api } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError, returnServerError } from '@/lib/utils/action' -import { CreateTeamSchema, UpdateTeamNameSchema } from '@/server/team/types' import type { CreateTeamsResponse } from '@/types/billing.types' export const updateTeamNameAction = authActionClient @@ -24,21 +26,15 @@ export const updateTeamNameAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { name, teamIdOrSlug } = parsedInput - const { teamId, session } = ctx + const result = await ctx.services.teams.updateTeamName(name) - const { data, error } = await api.PATCH('/teams/{teamId}', { - params: { path: { teamId } }, - headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), - body: { name }, - }) - - if (error) { - return returnServerError('Failed to update team name') + if (!result.ok) { + return toActionErrorFromRepoError(result.error) } revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') - return data + return result.data }) const AddTeamMemberSchema = z.object({ @@ -52,18 +48,10 @@ export const addTeamMemberAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { email, teamIdOrSlug } = parsedInput - const { teamId, session } = ctx - - const { error } = await api.POST('/teams/{teamId}/members', { - params: { path: { teamId } }, - headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), - body: { email }, - }) + const result = await ctx.services.teams.addTeamMember(email) - if (error) { - const message = - (error as { message?: string }).message ?? 'Failed to add team member' - return returnServerError(message) + if (!result.ok) { + return toActionErrorFromRepoError(result.error) } revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') @@ -80,18 +68,10 @@ export const removeTeamMemberAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { userId, teamIdOrSlug } = parsedInput - const { teamId, session } = ctx + const result = await ctx.services.teams.removeTeamMember(userId) - const { error } = await api.DELETE('/teams/{teamId}/members/{userId}', { - params: { path: { teamId, userId } }, - headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }) - - if (error) { - const message = - (error as { message?: string }).message ?? - 'Failed to remove team member' - return returnServerError(message) + if (!result.ok) { + return toActionErrorFromRepoError(result.error) } revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') @@ -142,7 +122,7 @@ export const uploadTeamProfilePictureAction = authActionClient .use(withTeamIdResolution) .action(async ({ parsedInput, ctx }) => { const { image, teamIdOrSlug } = parsedInput - const { teamId } = ctx + const { teamId, services } = ctx const allowedTypes = ['image/jpeg', 'image/png'] @@ -189,16 +169,9 @@ export const uploadTeamProfilePictureAction = authActionClient const publicUrl = await uploadFile(buffer, storagePath, fileType.mime) - // profile_picture_url stays on supabase admin — tightly coupled to supabase storage - const { data, error } = await supabaseAdmin - .from('teams') - .update({ profile_picture_url: publicUrl }) - .eq('id', teamId) - .select() - .single() - - if (error) { - throw new Error(error.message) + const result = await services.teams.updateTeamProfilePictureUrl(publicUrl) + if (!result.ok) { + throw new Error(result.error.message) } after(async () => { @@ -229,5 +202,5 @@ export const uploadTeamProfilePictureAction = authActionClient revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') - return data + return result.data }) diff --git a/src/server/user/user-actions.ts b/src/core/server/actions/user-actions.ts similarity index 100% rename from src/server/user/user-actions.ts rename to src/core/server/actions/user-actions.ts diff --git a/src/server/webhooks/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts similarity index 63% rename from src/server/webhooks/webhooks-actions.ts rename to src/core/server/actions/webhooks-actions.ts index 91adebac3..7744334fb 100644 --- a/src/server/webhooks/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -2,17 +2,15 @@ import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { handleDefaultInfraError } from '@/lib/utils/action' import { DeleteWebhookSchema, UpdateWebhookSecretSchema, UpsertWebhookSchema, -} from './schema' +} from '@/core/server/functions/webhooks/schema' +import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { l } from '@/lib/clients/logger/logger' +import { handleDefaultInfraError } from '@/lib/utils/action' // Upsert Webhook (Create or Update) @@ -28,45 +26,25 @@ export const upsertWebhookAction = authActionClient parsedInput const { session, teamId } = ctx - const accessToken = session.access_token - const isEdit = mode === 'edit' - - const response = isEdit - ? await infra.PATCH('/events/webhooks/{webhookID}', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - params: { - path: { webhookID: webhookId! }, - }, - body: { - name, - url, - events, - enabled, - }, - }) - : await infra.POST('/events/webhooks', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - body: { - name, - url, - events, - enabled, - signatureSecret: signatureSecret!, - }, - }) + const response = await ctx.services.webhooks.upsertWebhook({ + mode: mode === 'add' ? 'create' : 'edit', + webhookId: webhookId ?? undefined, + name, + url, + events, + signatureSecret: signatureSecret ?? undefined, + enabled, + }) - if (response.error) { - const status = response.response.status + if (!response.ok) { + const status = response.error.status l.error( { - key: isEdit - ? 'update_webhook:infra_error' - : 'create_webhook:infra_error', + key: + mode === 'edit' + ? 'update_webhook:infra_error' + : 'create_webhook:infra_error', error: response.error, team_id: teamId, user_id: session.user.id, @@ -79,7 +57,7 @@ export const upsertWebhookAction = authActionClient events, }, }, - `Failed to ${isEdit ? 'update' : 'create'} webhook: ${status}: ${response.error.message}` + `Failed to ${mode === 'edit' ? 'update' : 'create'} webhook: ${status}: ${response.error.message}` ) return handleDefaultInfraError(status) @@ -104,19 +82,10 @@ export const deleteWebhookAction = authActionClient const { webhookId } = parsedInput const { session, teamId } = ctx - const accessToken = session.access_token + const response = await ctx.services.webhooks.deleteWebhook(webhookId) - const response = await infra.DELETE('/events/webhooks/{webhookID}', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - params: { - path: { webhookID: webhookId }, - }, - }) - - if (response.error) { - const status = response.response.status + if (!response.ok) { + const status = response.error.status l.error( { @@ -154,22 +123,13 @@ export const updateWebhookSecretAction = authActionClient const { webhookId, signatureSecret } = parsedInput const { session, teamId } = ctx - const accessToken = session.access_token - - const response = await infra.PATCH('/events/webhooks/{webhookID}', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - params: { - path: { webhookID: webhookId }, - }, - body: { - signatureSecret, - }, - }) + const response = await ctx.services.webhooks.updateWebhookSecret( + webhookId, + signatureSecret + ) - if (response.error) { - const status = response.response.status + if (!response.ok) { + const status = response.error.status l.error( { diff --git a/src/core/server/adapters/repo-error.ts b/src/core/server/adapters/repo-error.ts new file mode 100644 index 000000000..0c9134009 --- /dev/null +++ b/src/core/server/adapters/repo-error.ts @@ -0,0 +1,35 @@ +import { TRPCError } from '@trpc/server' +import type { RepoError } from '@/core/shared/result' +import { ActionError } from '@/lib/utils/action' + +function trpcCodeFromRepoError(code: RepoError['code']): TRPCError['code'] { + switch (code) { + case 'unauthorized': + return 'UNAUTHORIZED' + case 'forbidden': + return 'FORBIDDEN' + case 'not_found': + return 'NOT_FOUND' + case 'validation': + return 'BAD_REQUEST' + case 'conflict': + return 'CONFLICT' + default: + return 'INTERNAL_SERVER_ERROR' + } +} + +export function throwTRPCErrorFromRepoError(error: RepoError): never { + throw new TRPCError({ + code: trpcCodeFromRepoError(error.code), + message: error.message, + }) +} + +export function toActionErrorFromRepoError(error: RepoError): never { + throw new ActionError(error.message) +} + +export function toRouteErrorResponse(error: RepoError): Response { + return Response.json({ error: error.message }, { status: error.status }) +} diff --git a/src/server/api/errors.ts b/src/core/server/adapters/trpc-errors.ts similarity index 100% rename from src/server/api/errors.ts rename to src/core/server/adapters/trpc-errors.ts diff --git a/src/server/api/middlewares/auth.ts b/src/core/server/api/middlewares/auth.ts similarity index 77% rename from src/server/api/middlewares/auth.ts rename to src/core/server/api/middlewares/auth.ts index 9c7ec3e8b..cec9e8f7b 100644 --- a/src/server/api/middlewares/auth.ts +++ b/src/core/server/api/middlewares/auth.ts @@ -4,11 +4,12 @@ import { parseCookieHeader, serializeCookieHeader, } from '@supabase/ssr' +import { unauthorizedUserError } from '@/core/server/adapters/trpc-errors' +import { createRequestContext } from '@/core/server/context/request-context' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import getUserByToken from '@/core/server/functions/auth/get-user-by-token' +import { t } from '@/core/server/trpc/init' import { getTracer } from '@/lib/clients/tracer' -import { getSessionInsecure } from '@/server/auth/get-session' -import getUserByToken from '@/server/auth/get-user-by-token' -import { unauthorizedUserError } from '../errors' -import { t } from '../init' const createSupabaseServerClient = (headers: Headers) => { return createServerClient( @@ -20,12 +21,12 @@ const createSupabaseServerClient = (headers: Headers) => { return parseCookieHeader(headers.get('cookie') ?? '') }, setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => + cookiesToSet.forEach(({ name, value, options }) => { headers.append( 'Set-Cookie', serializeCookieHeader(name, value, options) ) - ) + }) }, }, } @@ -79,6 +80,9 @@ export const authMiddleware = t.middleware(async ({ ctx, next }) => { ...ctx, session, user, + services: createRequestContext({ + accessToken: session.access_token, + }).services, }, }) } finally { diff --git a/src/server/api/middlewares/telemetry.ts b/src/core/server/api/middlewares/telemetry.ts similarity index 98% rename from src/server/api/middlewares/telemetry.ts rename to src/core/server/api/middlewares/telemetry.ts index 573a0a53b..0ac4208e8 100644 --- a/src/server/api/middlewares/telemetry.ts +++ b/src/core/server/api/middlewares/telemetry.ts @@ -9,12 +9,12 @@ import { import type { User } from '@supabase/supabase-js' import { TRPCError } from '@trpc/server' import { serializeError } from 'serialize-error' +import { internalServerError } from '@/core/server/adapters/trpc-errors' +import { t } from '@/core/server/trpc/init' import { l } from '@/lib/clients/logger/logger' import { getMeter } from '@/lib/clients/meter' import { getTracer } from '@/lib/clients/tracer' import { flattenClientInputValue } from '@/lib/utils/action' -import { internalServerError } from '../errors' -import { t } from '../init' /** * Telemetry State diff --git a/src/core/server/api/routers/billing.ts b/src/core/server/api/routers/billing.ts new file mode 100644 index 000000000..519b72088 --- /dev/null +++ b/src/core/server/api/routers/billing.ts @@ -0,0 +1,130 @@ +import { TRPCError } from '@trpc/server' +import { headers } from 'next/headers' +import { z } from 'zod' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedTeamProcedure } from '@/core/server/trpc/procedures' +import { + ADDON_500_SANDBOXES_ID, + ADDON_PURCHASE_ACTION_ERRORS, +} from '@/features/dashboard/billing/constants' + +function limitTypeToKey(type: 'limit' | 'alert') { + return type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte' +} + +export const billingRouter = createTRPCRouter({ + createCheckout: protectedTeamProcedure + .input(z.object({ tierId: z.string() })) + .mutation(async ({ ctx, input }) => { + const result = await ctx.services.billing.createCheckout(input.tierId) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + createCustomerPortalSession: protectedTeamProcedure.mutation( + async ({ ctx }) => { + const origin = (await headers()).get('origin') + const result = + await ctx.services.billing.createCustomerPortalSession(origin) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return { url: result.data.url } + } + ), + + getItems: protectedTeamProcedure.query(async ({ ctx }) => { + const result = await ctx.services.billing.getItems() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + getUsage: protectedTeamProcedure.query(async ({ ctx }) => { + const result = await ctx.services.billing.getUsage() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + getInvoices: protectedTeamProcedure.query(async ({ ctx }) => { + const result = await ctx.services.billing.getInvoices() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + getLimits: protectedTeamProcedure.query(async ({ ctx }) => { + const result = await ctx.services.billing.getLimits() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + getTeamLimits: protectedTeamProcedure.query(async ({ ctx }) => { + const limitsResult = await ctx.services.teams.getTeamLimitsByIdOrSlug( + ctx.teamId + ) + if (!limitsResult.ok) { + throwTRPCErrorFromRepoError(limitsResult.error) + } + + return limitsResult.data + }), + + setLimit: protectedTeamProcedure + .input( + z.object({ + type: z.enum(['limit', 'alert']), + value: z.number().min(1), + }) + ) + .mutation(async ({ ctx, input }) => { + const { type, value } = input + const result = await ctx.services.billing.setLimit( + limitTypeToKey(type), + value + ) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + }), + + clearLimit: protectedTeamProcedure + .input(z.object({ type: z.enum(['limit', 'alert']) })) + .mutation(async ({ ctx, input }) => { + const { type } = input + const result = await ctx.services.billing.clearLimit(limitTypeToKey(type)) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + }), + + createOrder: protectedTeamProcedure + .input(z.object({ itemId: z.literal(ADDON_500_SANDBOXES_ID) })) + .mutation(async ({ ctx, input }) => { + const { itemId } = input + const result = await ctx.services.billing.createOrder(itemId) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + confirmOrder: protectedTeamProcedure + .input(z.object({ orderId: z.string().uuid() })) + .mutation(async ({ ctx, input }) => { + const { orderId } = input + const result = await ctx.services.billing.confirmOrder(orderId) + if (!result.ok) { + if ( + result.error.message.includes( + 'Missing payment method, please update your payment information' + ) + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: ADDON_PURCHASE_ACTION_ERRORS.missingPaymentMethod, + }) + } + throwTRPCErrorFromRepoError(result.error) + } + + return result.data + }), + + getCustomerSession: protectedTeamProcedure.mutation(async ({ ctx }) => { + const result = await ctx.services.billing.getCustomerSession() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), +}) diff --git a/src/server/api/routers/builds.ts b/src/core/server/api/routers/builds.ts similarity index 66% rename from src/server/api/routers/builds.ts rename to src/core/server/api/routers/builds.ts index 327447c4a..6eb46ca5e 100644 --- a/src/server/api/routers/builds.ts +++ b/src/core/server/api/routers/builds.ts @@ -1,14 +1,13 @@ import { z } from 'zod' -import { LOG_RETENTION_MS } from '@/features/dashboard/templates/builds/constants' -import { buildsRepo } from '@/server/api/repositories/builds.repository' -import { createTRPCRouter } from '../init' import { - type BuildDetailsDTO, - type BuildLogDTO, - type BuildLogsDTO, + type BuildDetailsModel, + type BuildLogModel, + type BuildLogsModel, BuildStatusSchema, -} from '../models/builds.models' -import { protectedTeamProcedure } from '../procedures' +} from '@/core/domains/builds/models' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedTeamProcedure } from '@/core/server/trpc/procedures' +import { LOG_RETENTION_MS } from '@/features/dashboard/templates/builds/constants' export const buildsRouter = createTRPCRouter({ // QUERIES @@ -23,16 +22,12 @@ export const buildsRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId } = ctx const { buildIdOrTemplate, statuses, limit, cursor } = input - return await buildsRepo.listBuilds( - ctx.session.access_token, - teamId, - buildIdOrTemplate, - statuses, - { limit, cursor } - ) + return await ctx.services.builds.listBuilds(buildIdOrTemplate, statuses, { + limit, + cursor, + }) }), runningStatuses: protectedTeamProcedure @@ -42,14 +37,9 @@ export const buildsRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId } = ctx const { buildIds } = input - return await buildsRepo.getRunningStatuses( - ctx.session.access_token, - teamId, - buildIds - ) + return await ctx.services.builds.getRunningStatuses(buildIds) }), buildDetails: protectedTeamProcedure @@ -60,16 +50,11 @@ export const buildsRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId } = ctx const { buildId, templateId } = input - const buildInfo = await buildsRepo.getBuildInfo( - ctx.session.access_token, - buildId, - teamId - ) + const buildInfo = await ctx.services.builds.getBuildInfo(buildId) - const result: BuildDetailsDTO = { + const result: BuildDetailsModel = { templateNames: buildInfo.names, template: buildInfo.names?.[0] ?? templateId, startedAt: buildInfo.createdAt, @@ -92,24 +77,21 @@ export const buildsRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId } = ctx const { buildId, templateId, level } = input let { cursor } = input - cursor ??= new Date().getTime() + cursor ??= Date.now() const direction = 'backward' const limit = 100 - const buildLogs = await buildsRepo.getInfraBuildLogs( - ctx.session.access_token, - teamId, + const buildLogs = await ctx.services.builds.getInfraBuildLogs( templateId, buildId, { cursor, limit, direction, level } ) - const logs: BuildLogDTO[] = buildLogs.logs + const logs: BuildLogModel[] = buildLogs.logs .map((log) => ({ timestampUnix: new Date(log.timestamp).getTime(), level: log.level, @@ -121,7 +103,7 @@ export const buildsRouter = createTRPCRouter({ const cursorLog = logs[0] const nextCursor = hasMore ? (cursorLog?.timestampUnix ?? null) : null - const result: BuildLogsDTO = { + const result: BuildLogsModel = { logs, nextCursor, } @@ -139,33 +121,36 @@ export const buildsRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId } = ctx const { buildId, templateId, level } = input let { cursor } = input - cursor ??= new Date().getTime() + cursor ??= Date.now() const direction = 'forward' const limit = 100 - const buildLogs = await buildsRepo.getInfraBuildLogs( - ctx.session.access_token, - teamId, + const buildLogs = await ctx.services.builds.getInfraBuildLogs( templateId, buildId, { cursor, limit, direction, level } ) - const logs: BuildLogDTO[] = buildLogs.logs.map((log) => ({ - timestampUnix: new Date(log.timestamp).getTime(), - level: log.level, - message: log.message, - })) + const logs: BuildLogModel[] = buildLogs.logs.map( + (log: { + timestamp: string + level: BuildLogModel['level'] + message: string + }) => ({ + timestampUnix: new Date(log.timestamp).getTime(), + level: log.level, + message: log.message, + }) + ) const newestLog = logs[logs.length - 1] const nextCursor = newestLog?.timestampUnix ?? cursor - const result: BuildLogsDTO = { + const result: BuildLogsModel = { logs, nextCursor, } @@ -175,5 +160,5 @@ export const buildsRouter = createTRPCRouter({ }) function checkIfBuildStillHasLogs(createdAt: number): boolean { - return new Date().getTime() - createdAt < LOG_RETENTION_MS + return Date.now() - createdAt < LOG_RETENTION_MS } diff --git a/src/server/api/routers/index.ts b/src/core/server/api/routers/index.ts similarity index 91% rename from src/server/api/routers/index.ts rename to src/core/server/api/routers/index.ts index c8f492117..530503282 100644 --- a/src/server/api/routers/index.ts +++ b/src/core/server/api/routers/index.ts @@ -1,4 +1,4 @@ -import { createCallerFactory, createTRPCRouter } from '../init' +import { createCallerFactory, createTRPCRouter } from '@/core/server/trpc/init' import { billingRouter } from './billing' import { buildsRouter } from './builds' import { sandboxRouter } from './sandbox' diff --git a/src/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts similarity index 71% rename from src/server/api/routers/sandbox.ts rename to src/core/server/api/routers/sandbox.ts index 649822fbc..fde605a5c 100644 --- a/src/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -1,19 +1,18 @@ import { millisecondsInDay } from 'date-fns/constants' import { z } from 'zod' -import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' -import { SandboxIdSchema } from '@/lib/schemas/api' -import { createTRPCRouter } from '../init' import { deriveSandboxLifecycleFromEvents, - mapApiSandboxRecordToDTO, - mapInfraSandboxDetailsToDTO, - mapInfraSandboxLogToDTO, - type SandboxDetailsDTO, - type SandboxLogDTO, - type SandboxLogsDTO, -} from '../models/sandboxes.models' -import { protectedTeamProcedure } from '../procedures' -import { sandboxesRepo } from '../repositories/sandboxes.repository' + mapApiSandboxRecordToModel, + mapInfraSandboxDetailsToModel, + mapInfraSandboxLogToModel, + type SandboxDetailsModel, + type SandboxLogModel, + type SandboxLogsModel, +} from '@/core/domains/sandboxes/models' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedTeamProcedure } from '@/core/server/trpc/procedures' +import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' +import { SandboxIdSchema } from '@/lib/schemas/api' export const sandboxRouter = createTRPCRouter({ // QUERIES @@ -25,25 +24,18 @@ export const sandboxRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId, session } = ctx const { sandboxId } = input - const detailsResult = await sandboxesRepo.getSandboxDetails( - session.access_token, - teamId, - sandboxId - ) + const detailsResult = + await ctx.services.sandboxes.getSandboxDetails(sandboxId) - const mappedDetails: SandboxDetailsDTO = + const mappedDetails: SandboxDetailsModel = detailsResult.source === 'infra' - ? mapInfraSandboxDetailsToDTO(detailsResult.details) - : mapApiSandboxRecordToDTO(detailsResult.details) + ? mapInfraSandboxDetailsToModel(detailsResult.details) + : mapApiSandboxRecordToModel(detailsResult.details) - const lifecycleEvents = await sandboxesRepo.getSandboxLifecycleEvents( - session.access_token, - teamId, - sandboxId - ) + const lifecycleEvents = + await ctx.services.sandboxes.getSandboxLifecycleEvents(sandboxId) const derivedLifecycle = deriveSandboxLifecycleFromEvents(lifecycleEvents) const fallbackPausedAt = mappedDetails.state === 'paused' ? mappedDetails.endAt : null @@ -73,7 +65,6 @@ export const sandboxRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId, session } = ctx const { sandboxId, level, search } = input let { cursor } = input @@ -82,22 +73,20 @@ export const sandboxRouter = createTRPCRouter({ const direction = 'backward' const limit = 100 - const sandboxLogs = await sandboxesRepo.getSandboxLogs( - session.access_token, - teamId, + const sandboxLogs = await ctx.services.sandboxes.getSandboxLogs( sandboxId, { cursor, limit, direction, level, search } ) - const logs: SandboxLogDTO[] = sandboxLogs.logs - .map(mapInfraSandboxLogToDTO) + const logs: SandboxLogModel[] = sandboxLogs.logs + .map(mapInfraSandboxLogToModel) .reverse() const hasMore = logs.length === limit const cursorLog = logs[0] const nextCursor = hasMore ? (cursorLog?.timestampUnix ?? null) : null - const result: SandboxLogsDTO = { + const result: SandboxLogsModel = { logs, nextCursor, } @@ -115,7 +104,6 @@ export const sandboxRouter = createTRPCRouter({ }) ) .query(async ({ ctx, input }) => { - const { teamId, session } = ctx const { sandboxId, level, search } = input let { cursor } = input @@ -124,21 +112,19 @@ export const sandboxRouter = createTRPCRouter({ const direction = 'forward' const limit = 100 - const sandboxLogs = await sandboxesRepo.getSandboxLogs( - session.access_token, - teamId, + const sandboxLogs = await ctx.services.sandboxes.getSandboxLogs( sandboxId, { cursor, limit, direction, level, search } ) - const logs: SandboxLogDTO[] = sandboxLogs.logs.map( - mapInfraSandboxLogToDTO + const logs: SandboxLogModel[] = sandboxLogs.logs.map( + mapInfraSandboxLogToModel ) const newestLog = logs[logs.length - 1] const nextCursor = newestLog?.timestampUnix ?? cursor - const result: SandboxLogsDTO = { + const result: SandboxLogsModel = { logs, nextCursor, } @@ -172,13 +158,10 @@ export const sandboxRouter = createTRPCRouter({ ) ) .query(async ({ ctx, input }) => { - const { teamId, session } = ctx const { sandboxId } = input const { startMs, endMs } = input - const metrics = await sandboxesRepo.getSandboxMetrics( - session.access_token, - teamId, + const metrics = await ctx.services.sandboxes.getSandboxMetrics( sandboxId, { startUnixMs: startMs, diff --git a/src/core/server/api/routers/sandboxes.ts b/src/core/server/api/routers/sandboxes.ts new file mode 100644 index 000000000..a1ed31083 --- /dev/null +++ b/src/core/server/api/routers/sandboxes.ts @@ -0,0 +1,156 @@ +import { z } from 'zod' +import { USE_MOCK_DATA } from '@/configs/flags' +import { + calculateTeamMetricsStep, + MOCK_SANDBOXES_DATA, + MOCK_TEAM_METRICS_DATA, + MOCK_TEAM_METRICS_MAX_DATA, +} from '@/configs/mock-data' +import { + GetTeamMetricsMaxSchema, + GetTeamMetricsSchema, +} from '@/core/domains/sandboxes/schemas' +import { + fillTeamMetricsWithZeros, + transformMetricsToClientMetrics, +} from '@/core/server/functions/sandboxes/utils' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedTeamProcedure } from '@/core/server/trpc/procedures' + +export const sandboxesRouter = createTRPCRouter({ + // QUERIES + getSandboxes: protectedTeamProcedure.query(async ({ ctx }) => { + if (USE_MOCK_DATA) { + await new Promise((resolve) => setTimeout(resolve, 200)) + + const sandboxes = MOCK_SANDBOXES_DATA() + + return { + sandboxes, + } + } + + const sandboxes = await ctx.services.sandboxes.listSandboxes() + + return { + sandboxes, + } + }), + + getSandboxesMetrics: protectedTeamProcedure + .input( + z.object({ + sandboxIds: z.array(z.string()), + }) + ) + .query(async ({ ctx, input }) => { + const { sandboxIds } = input + + if (sandboxIds.length === 0 || USE_MOCK_DATA) { + return { + metrics: {}, + } + } + + const metricsData = + await ctx.services.sandboxes.getSandboxesMetrics(sandboxIds) + const metrics = transformMetricsToClientMetrics(metricsData) + + return { + metrics, + } + }), + + getTeamMetrics: protectedTeamProcedure + .input(GetTeamMetricsSchema) + .query(async ({ ctx, input }) => { + const { startDate: startDateMs, endDate: endDateMs } = input + + // use mock data if enabled + if (USE_MOCK_DATA) { + const mockData = MOCK_TEAM_METRICS_DATA(startDateMs, endDateMs) + const filledMetrics = fillTeamMetricsWithZeros( + mockData.metrics, + startDateMs, + endDateMs, + mockData.step + ) + return { + metrics: filledMetrics, + step: mockData.step, + } + } + + const startS = Math.floor(startDateMs / 1000) + const endS = Math.floor(endDateMs / 1000) + + // calculate step to determine overfetch amount + const stepMs = calculateTeamMetricsStep(startDateMs, endDateMs) + + // overfetch by one step + // the overfetch is accounted for when post-processing the data using fillTeamMetricsWithZeros + const overfetchS = Math.ceil(stepMs / 1000) + + const metricData = await ctx.services.sandboxes.getTeamMetricsRange( + startS, + endS + overfetchS + ) + + // transform timestamps from seconds to milliseconds + const metrics = metricData.map( + (d: { + concurrentSandboxes: number + sandboxStartRate: number + timestampUnix: number + }) => ({ + concurrentSandboxes: d.concurrentSandboxes, + sandboxStartRate: d.sandboxStartRate, + timestamp: d.timestampUnix * 1000, + }) + ) + + // fill gaps with zeros for smooth visualization + const filledMetrics = fillTeamMetricsWithZeros( + metrics, + startDateMs, + endDateMs, + stepMs + ) + + return { + metrics: filledMetrics, + step: stepMs, + } + }), + + getTeamMetricsMax: protectedTeamProcedure + .input(GetTeamMetricsMaxSchema) + .query(async ({ ctx, input }) => { + const { startDate: startDateMs, endDate: endDateMs, metric } = input + + if (USE_MOCK_DATA) { + return MOCK_TEAM_METRICS_MAX_DATA(startDateMs, endDateMs, metric) + } + + // convert milliseconds to seconds for the API + const startS = Math.floor(startDateMs / 1000) + const endS = Math.floor(endDateMs / 1000) + + const maxMetric = await ctx.services.sandboxes.getTeamMetricsMax( + startS, + endS, + metric + ) + + // since javascript timestamps are in milliseconds, we want to convert the timestamp back to milliseconds + const timestampMs = maxMetric.timestampUnix * 1000 + + return { + timestamp: timestampMs, + value: maxMetric.value, + metric, + } + }), + + // MUTATIONS +}) diff --git a/src/server/api/routers/support.ts b/src/core/server/api/routers/support.ts similarity index 74% rename from src/server/api/routers/support.ts rename to src/core/server/api/routers/support.ts index 71ba2a5c4..2ce1eee04 100644 --- a/src/server/api/routers/support.ts +++ b/src/core/server/api/routers/support.ts @@ -1,8 +1,7 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' -import { createTRPCRouter } from '../init' -import { protectedTeamProcedure } from '../procedures' -import { supportRepo } from '../repositories/support.repository' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { protectedTeamProcedure } from '@/core/server/trpc/procedures' const E2B_API_KEY_REGEX = /e2b_[a-f0-9]{40}/i @@ -21,7 +20,7 @@ export const supportRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { - const { teamId, session, user } = ctx + const { teamId, user } = ctx const email = user.email if (!email) { @@ -36,12 +35,9 @@ export const supportRouter = createTRPCRouter({ }) } - const team = await supportRepo.getTeamSupportData( - teamId, - session.access_token - ) + const team = await ctx.services.support.getTeamSupportData() - return supportRepo.createSupportThread({ + return ctx.services.support.createSupportThread({ description: input.description, files: input.files, teamId, diff --git a/src/core/server/api/routers/teams.ts b/src/core/server/api/routers/teams.ts new file mode 100644 index 000000000..d7b6ee3ab --- /dev/null +++ b/src/core/server/api/routers/teams.ts @@ -0,0 +1,20 @@ +import z from 'zod' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { protectedProcedure } from '@/core/server/trpc/procedures' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' + +export const teamsRouter = { + getCurrentTeam: protectedProcedure + .input(z.object({ teamIdOrSlug: TeamIdOrSlugSchema })) + .query(async ({ ctx, input }) => { + const teamResult = await ctx.services.teams.getCurrentUserTeam( + input.teamIdOrSlug + ) + + if (!teamResult.ok) { + throwTRPCErrorFromRepoError(teamResult.error) + } + + return teamResult.data + }), +} diff --git a/src/core/server/api/routers/templates.ts b/src/core/server/api/routers/templates.ts new file mode 100644 index 000000000..2702fa18c --- /dev/null +++ b/src/core/server/api/routers/templates.ts @@ -0,0 +1,75 @@ +import { TRPCError } from '@trpc/server' +import { z } from 'zod' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { createTRPCRouter } from '@/core/server/trpc/init' +import { + protectedProcedure, + protectedTeamProcedure, +} from '@/core/server/trpc/procedures' + +export const templatesRouter = createTRPCRouter({ + // QUERIES + + getTemplates: protectedTeamProcedure.query(async ({ ctx }) => { + const result = await ctx.services.templates.getTeamTemplates() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + getDefaultTemplatesCached: protectedProcedure.query(async ({ ctx }) => { + const result = await ctx.services.templates.getDefaultTemplatesCached() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + }), + + // MUTATIONS + + deleteTemplate: protectedTeamProcedure + .input( + z.object({ + templateId: z.string(), + }) + ) + .mutation(async ({ ctx, input }) => { + const { templateId } = input + + const result = await ctx.services.templates.deleteTemplate(templateId) + + if (!result.ok) { + if ( + result.error.status === 400 && + result.error.message.includes( + 'because there are paused sandboxes using it' + ) + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: + 'Cannot delete template because there are paused sandboxes using it', + }) + } + throwTRPCErrorFromRepoError(result.error) + } + + return result.data + }), + + updateTemplate: protectedTeamProcedure + .input( + z.object({ + templateId: z.string(), + public: z.boolean(), + }) + ) + .mutation(async ({ ctx, input }) => { + const { templateId, public: isPublic } = input + + const result = await ctx.services.templates.updateTemplateVisibility( + templateId, + isPublic + ) + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + + return result.data + }), +}) diff --git a/src/core/server/context/from-route.ts b/src/core/server/context/from-route.ts new file mode 100644 index 000000000..51865626e --- /dev/null +++ b/src/core/server/context/from-route.ts @@ -0,0 +1,11 @@ +import { createRequestContext } from './request-context' + +export function createRouteServices(input: { + accessToken: string + teamId?: string +}) { + return createRequestContext({ + accessToken: input.accessToken, + teamId: input.teamId, + }).services +} diff --git a/src/core/server/context/request-context.ts b/src/core/server/context/request-context.ts new file mode 100644 index 000000000..630782817 --- /dev/null +++ b/src/core/server/context/request-context.ts @@ -0,0 +1,68 @@ +import { createBillingRepository } from '@/core/domains/billing/repository.server' +import { createBuildsRepository } from '@/core/domains/builds/repository.server' +import { createKeysRepository } from '@/core/domains/keys/repository.server' +import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' +import { createSupportRepository } from '@/core/domains/support/repository.server' +import { createTeamsRepository } from '@/core/domains/teams/repository.server' +import { createTemplatesRepository } from '@/core/domains/templates/repository.server' +import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' + +export interface RequestScope { + accessToken: string + teamId?: string +} + +function buildRequestServices(scope: RequestScope) { + const requireTeamScope = () => { + if (!scope.teamId) { + throw new Error('teamId is required in request scope') + } + + return { + accessToken: scope.accessToken, + teamId: scope.teamId, + } + } + + return { + teams: createTeamsRepository(scope), + get builds() { + return createBuildsRepository(requireTeamScope()) + }, + get sandboxes() { + return createSandboxesRepository(requireTeamScope()) + }, + get templates() { + return createTemplatesRepository(requireTeamScope()) + }, + get billing() { + return createBillingRepository(requireTeamScope()) + }, + support: createSupportRepository(scope), + get keys() { + return createKeysRepository(requireTeamScope()) + }, + get webhooks() { + return createWebhooksRepository(requireTeamScope()) + }, + } +} + +export type RequestContextServices = ReturnType + +export interface RequestContext { + scope: RequestScope + services: RequestContextServices +} + +export function createRequestContext(scope: RequestScope): RequestContext { + let services: RequestContextServices | undefined + + return { + scope, + get services() { + services ??= buildRequestServices(scope) + return services + }, + } +} diff --git a/src/server/auth/auth.types.ts b/src/core/server/functions/auth/auth.types.ts similarity index 100% rename from src/server/auth/auth.types.ts rename to src/core/server/functions/auth/auth.types.ts diff --git a/src/server/auth/get-session.ts b/src/core/server/functions/auth/get-session.ts similarity index 100% rename from src/server/auth/get-session.ts rename to src/core/server/functions/auth/get-session.ts diff --git a/src/server/auth/get-user-by-token.ts b/src/core/server/functions/auth/get-user-by-token.ts similarity index 100% rename from src/server/auth/get-user-by-token.ts rename to src/core/server/functions/auth/get-user-by-token.ts diff --git a/src/server/auth/validate-email.ts b/src/core/server/functions/auth/validate-email.ts similarity index 100% rename from src/server/auth/validate-email.ts rename to src/core/server/functions/auth/validate-email.ts diff --git a/src/server/keys/get-api-keys.ts b/src/core/server/functions/keys/get-api-keys.ts similarity index 72% rename from src/server/keys/get-api-keys.ts rename to src/core/server/functions/keys/get-api-keys.ts index 14b519152..97993b255 100644 --- a/src/server/keys/get-api-keys.ts +++ b/src/core/server/functions/keys/get-api-keys.ts @@ -2,10 +2,8 @@ import 'server-only' import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' @@ -25,20 +23,14 @@ export const getTeamApiKeys = authActionClient const { session, teamId } = ctx - const accessToken = session.access_token + const result = await ctx.services.keys.listTeamApiKeys() - const res = await infra.GET('/api-keys', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (res.error) { - const status = res.response.status + if (!result.ok) { + const status = result.error.status l.error({ key: 'get_team_api_keys:error', - error: res.error, + error: result.error, team_id: teamId, user_id: session.user.id, context: { @@ -49,5 +41,5 @@ export const getTeamApiKeys = authActionClient return handleDefaultInfraError(status) } - return { apiKeys: res.data } + return { apiKeys: result.data } }) diff --git a/src/server/keys/types.ts b/src/core/server/functions/keys/types.ts similarity index 100% rename from src/server/keys/types.ts rename to src/core/server/functions/keys/types.ts diff --git a/src/server/sandboxes/get-team-metrics-core.ts b/src/core/server/functions/sandboxes/get-team-metrics-core.ts similarity index 97% rename from src/server/sandboxes/get-team-metrics-core.ts rename to src/core/server/functions/sandboxes/get-team-metrics-core.ts index 67262e638..3633c4df7 100644 --- a/src/server/sandboxes/get-team-metrics-core.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-core.ts @@ -7,10 +7,10 @@ import { calculateTeamMetricsStep, MOCK_TEAM_METRICS_DATA, } from '@/configs/mock-data' +import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' -import { fillTeamMetricsWithZeros } from '@/server/sandboxes/utils' import type { ClientTeamMetrics } from '@/types/sandboxes.types' interface GetTeamMetricsCoreParams { diff --git a/src/server/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts similarity index 100% rename from src/server/sandboxes/get-team-metrics-max.ts rename to src/core/server/functions/sandboxes/get-team-metrics-max.ts diff --git a/src/server/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts similarity index 100% rename from src/server/sandboxes/get-team-metrics.ts rename to src/core/server/functions/sandboxes/get-team-metrics.ts diff --git a/src/server/sandboxes/utils.ts b/src/core/server/functions/sandboxes/utils.ts similarity index 100% rename from src/server/sandboxes/utils.ts rename to src/core/server/functions/sandboxes/utils.ts diff --git a/src/server/team/get-team-id-from-segment.ts b/src/core/server/functions/team/get-team-id-from-segment.ts similarity index 69% rename from src/server/team/get-team-id-from-segment.ts rename to src/core/server/functions/team/get-team-id-from-segment.ts index d266bd8f4..77c5a74ff 100644 --- a/src/server/team/get-team-id-from-segment.ts +++ b/src/core/server/functions/team/get-team-id-from-segment.ts @@ -1,9 +1,8 @@ import 'server-only' import z from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' -import { api } from '@/lib/clients/api' +import { createTeamsRepository } from '@/core/domains/teams/repository.server' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' @@ -27,13 +26,13 @@ export const getTeamIdFromSegment = async ( return segment } - const { data, error } = await api.GET('/teams/resolve', { - params: { query: { slug: segment } }, - headers: SUPABASE_AUTH_HEADERS(accessToken), - next: { tags: [CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)] }, + const resolvedTeam = await createTeamsRepository({ + accessToken, + }).resolveTeamBySlug(segment, { + tags: [CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)], }) - if (error || !data) { + if (!resolvedTeam.ok) { l.warn( { key: 'get_team_id_from_segment:resolve_failed', @@ -45,5 +44,5 @@ export const getTeamIdFromSegment = async ( return null } - return data.id + return resolvedTeam.data.id } diff --git a/src/server/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts similarity index 52% rename from src/server/team/get-team-limits.ts rename to src/core/server/functions/team/get-team-limits.ts index c7b45a511..0b4a7f1a1 100644 --- a/src/server/team/get-team-limits.ts +++ b/src/core/server/functions/team/get-team-limits.ts @@ -1,12 +1,10 @@ import 'server-only' import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { USE_MOCK_DATA } from '@/configs/flags' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { api } from '@/lib/clients/api' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' export interface TeamLimits { concurrentInstances: number @@ -33,31 +31,17 @@ export const getTeamLimits = authActionClient .metadata({ serverFunctionName: 'getTeamLimits' }) .use(withTeamIdResolution) .action(async ({ ctx }) => { - const { teamId, session } = ctx - if (USE_MOCK_DATA) { return MOCK_TIER_LIMITS } - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(session.access_token), - }) - - if (error || !data?.teams) { - return returnServerError('Failed to fetch team limits') - } - - const team = data.teams.find((t) => t.id === teamId) + const limitsResult = await ctx.services.teams.getTeamLimitsByIdOrSlug( + ctx.teamId + ) - if (!team) { - return returnServerError('Team not found') + if (!limitsResult.ok) { + return toActionErrorFromRepoError(limitsResult.error) } - return { - concurrentInstances: team.limits.concurrentSandboxes, - diskMb: team.limits.diskMb, - maxLengthHours: team.limits.maxLengthHours, - maxRamMb: team.limits.maxRamMb, - maxVcpu: team.limits.maxVcpu, - } satisfies TeamLimits + return limitsResult.data }) diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts new file mode 100644 index 000000000..8b45d8684 --- /dev/null +++ b/src/core/server/functions/team/get-team-members.ts @@ -0,0 +1,22 @@ +import 'server-only' + +import { z } from 'zod' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' + +const GetTeamMembersSchema = z.object({ + teamIdOrSlug: TeamIdOrSlugSchema, +}) + +export const getTeamMembers = authActionClient + .schema(GetTeamMembersSchema) + .metadata({ serverFunctionName: 'getTeamMembers' }) + .use(withTeamIdResolution) + .action(async ({ ctx }) => { + const result = await ctx.services.teams.listTeamMembers() + if (!result.ok) { + return toActionErrorFromRepoError(result.error) + } + return result.data + }) diff --git a/src/core/server/functions/team/get-team.ts b/src/core/server/functions/team/get-team.ts new file mode 100644 index 000000000..3baba86ce --- /dev/null +++ b/src/core/server/functions/team/get-team.ts @@ -0,0 +1,37 @@ +import 'server-cli-only' + +import { z } from 'zod' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { returnServerError } from '@/lib/utils/action' + +const GetTeamSchema = z.object({ + teamIdOrSlug: TeamIdOrSlugSchema, +}) + +export const getTeam = authActionClient + .schema(GetTeamSchema) + .metadata({ serverFunctionName: 'getTeam' }) + .use(withTeamIdResolution) + .action(async ({ ctx }) => { + const teamResult = await ctx.services.teams.getCurrentUserTeam(ctx.teamId) + + if (!teamResult.ok) { + return toActionErrorFromRepoError(teamResult.error) + } + + return teamResult.data + }) + +export const getUserTeams = authActionClient + .metadata({ serverFunctionName: 'getUserTeams' }) + .action(async ({ ctx }) => { + const teamsResult = await ctx.services.teams.listUserTeams() + + if (!teamsResult.ok || teamsResult.data.length === 0) { + return returnServerError('No teams found.') + } + + return teamsResult.data + }) diff --git a/src/server/team/resolve-user-team.ts b/src/core/server/functions/team/resolve-user-team.ts similarity index 65% rename from src/server/team/resolve-user-team.ts rename to src/core/server/functions/team/resolve-user-team.ts index baf0cc686..01bc8c2be 100644 --- a/src/server/team/resolve-user-team.ts +++ b/src/core/server/functions/team/resolve-user-team.ts @@ -1,11 +1,10 @@ import 'server-only' import { cookies } from 'next/headers' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' -import { api } from '@/lib/clients/api' +import type { ResolvedTeam } from '@/core/domains/teams/models' +import { createTeamsRepository } from '@/core/domains/teams/repository.server' import { l } from '@/lib/clients/logger/logger' -import type { ResolvedTeam } from './types' export async function resolveUserTeam( accessToken: string @@ -19,11 +18,11 @@ export async function resolveUserTeam( return { id: cookieTeamId, slug: cookieTeamSlug } } - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(accessToken), - }) + const teamsResult = await createTeamsRepository({ + accessToken, + }).listUserTeams() - if (error || !data?.teams) { + if (!teamsResult.ok) { l.error( { key: 'resolve_user_team:api_error', @@ -33,12 +32,12 @@ export async function resolveUserTeam( return null } - if (data.teams.length === 0) { + if (teamsResult.data.length === 0) { return null } - const defaultTeam = data.teams.find((t) => t.isDefault) - const team = defaultTeam ?? data.teams[0] + const defaultTeam = teamsResult.data.find((t) => t.is_default) + const team = defaultTeam ?? teamsResult.data[0] if (!team) { return null diff --git a/src/core/server/functions/team/types.ts b/src/core/server/functions/team/types.ts new file mode 100644 index 000000000..3ac473e17 --- /dev/null +++ b/src/core/server/functions/team/types.ts @@ -0,0 +1,11 @@ +export type { + ResolvedTeam, + TeamMember, + TeamMemberInfo, + TeamMemberRelation, +} from '@/core/domains/teams/models' +export { + CreateTeamSchema, + TeamNameSchema, + UpdateTeamNameSchema, +} from '@/core/domains/teams/schemas' diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts new file mode 100644 index 000000000..f10430aef --- /dev/null +++ b/src/core/server/functions/usage/get-usage.ts @@ -0,0 +1,32 @@ +import 'server-only' + +import { cacheLife, cacheTag } from 'next/cache' +import { z } from 'zod' +import { CACHE_TAGS } from '@/configs/cache' +import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { returnServerError } from '@/lib/utils/action' + +const GetUsageAuthActionSchema = z.object({ + teamIdOrSlug: TeamIdOrSlugSchema, +}) + +export const getUsage = authActionClient + .schema(GetUsageAuthActionSchema) + .metadata({ serverFunctionName: 'getUsage' }) + .use(withTeamIdResolution) + .action(async ({ ctx }) => { + 'use cache' + + const { teamId } = ctx + + cacheLife('hours') + cacheTag(CACHE_TAGS.TEAM_USAGE(teamId)) + + const result = await ctx.services.billing.getUsage() + if (!result.ok) { + return returnServerError(result.error.message) + } + + return result.data + }) diff --git a/src/server/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts similarity index 57% rename from src/server/webhooks/get-webhooks.ts rename to src/core/server/functions/webhooks/get-webhooks.ts index 53fedc0c2..9ebe63fb9 100644 --- a/src/server/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -1,9 +1,7 @@ import 'server-only' import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' @@ -19,36 +17,23 @@ export const getWebhooks = authActionClient .action(async ({ ctx }) => { const { session, teamId } = ctx - const accessToken = session.access_token - - const response = await infra.GET('/events/webhooks', { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (response.error) { - const status = response.response.status - - if (status === 404) { - return { webhooks: [] } - } + const result = await ctx.services.webhooks.listWebhooks() + if (!result.ok) { + const status = result.error.status l.error( { key: 'get_webhooks:infra_error', status, - error: response.error, + error: result.error, team_id: teamId, user_id: session.user.id, }, - `Failed to get webhook: ${status}: ${response.error.message}` + `Failed to get webhook: ${status}: ${result.error.message}` ) return handleDefaultInfraError(status) } - const data = response.data - - return { webhooks: data } + return { webhooks: result.data } }) diff --git a/src/server/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts similarity index 100% rename from src/server/webhooks/schema.ts rename to src/core/server/functions/webhooks/schema.ts diff --git a/src/server/proxy.ts b/src/core/server/http/proxy.ts similarity index 100% rename from src/server/proxy.ts rename to src/core/server/http/proxy.ts diff --git a/src/server/api/init.ts b/src/core/server/trpc/init.ts similarity index 70% rename from src/server/api/init.ts rename to src/core/server/trpc/init.ts index 44f9523dc..8b9859ce0 100644 --- a/src/server/api/init.ts +++ b/src/core/server/trpc/init.ts @@ -1,6 +1,8 @@ +import type { Session, User } from '@supabase/supabase-js' import { initTRPC } from '@trpc/server' import superjson from 'superjson' import { flattenError, ZodError } from 'zod' +import type { RequestContextServices } from '@/core/server/context/request-context' /** * TRPC Context Factory @@ -10,6 +12,10 @@ import { flattenError, ZodError } from 'zod' export const createTRPCContext = async (opts: { headers: Headers }) => { return { ...opts, + session: undefined as Session | undefined, + user: undefined as User | undefined, + teamId: undefined as string | undefined, + services: undefined as RequestContextServices | undefined, } } diff --git a/src/server/api/procedures.ts b/src/core/server/trpc/procedures.ts similarity index 84% rename from src/server/api/procedures.ts rename to src/core/server/trpc/procedures.ts index 533aed02d..8e9d9d6e1 100644 --- a/src/server/api/procedures.ts +++ b/src/core/server/trpc/procedures.ts @@ -1,15 +1,16 @@ import { context, SpanStatusCode, trace } from '@opentelemetry/api' import z from 'zod' -import { getTracer } from '@/lib/clients/tracer' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { getTeamIdFromSegment } from '../team/get-team-id-from-segment' -import { forbiddenTeamAccessError } from './errors' -import { t } from './init' -import { authMiddleware } from './middlewares/auth' +import { forbiddenTeamAccessError } from '@/core/server/adapters/trpc-errors' +import { authMiddleware } from '@/core/server/api/middlewares/auth' import { endTelemetryMiddleware, startTelemetryMiddleware, -} from './middlewares/telemetry' +} from '@/core/server/api/middlewares/telemetry' +import { createRequestContext } from '@/core/server/context/request-context' +import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' +import { getTracer } from '@/lib/clients/tracer' +import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { t } from './init' /** * IMPORTANT: Telemetry Middleware Usage @@ -98,6 +99,10 @@ export const protectedTeamProcedure = t.procedure ctx: { ...ctx, teamId, + services: createRequestContext({ + accessToken: ctx.session.access_token, + teamId, + }).services, }, }) } finally { diff --git a/src/core/shared/errors.ts b/src/core/shared/errors.ts new file mode 100644 index 000000000..fc97fd5a1 --- /dev/null +++ b/src/core/shared/errors.ts @@ -0,0 +1,59 @@ +import type { RepoError, RepoErrorCode } from './result' + +export function createRepoError(input: { + code: RepoErrorCode + status: number + message: string + cause?: unknown +}): RepoError { + return { + code: input.code, + status: input.status, + message: input.message, + cause: input.cause, + } +} + +export function repoErrorFromHttp( + status: number, + message: string, + cause?: unknown +): RepoError { + switch (status) { + case 401: + return createRepoError({ + code: 'unauthorized', + status, + message, + cause, + }) + case 403: + return createRepoError({ + code: 'forbidden', + status, + message, + cause, + }) + case 404: + return createRepoError({ + code: 'not_found', + status, + message, + cause, + }) + case 409: + return createRepoError({ + code: 'conflict', + status, + message, + cause, + }) + default: + return createRepoError({ + code: status >= 500 ? 'unavailable' : 'internal', + status, + message, + cause, + }) + } +} diff --git a/src/core/shared/result.ts b/src/core/shared/result.ts new file mode 100644 index 000000000..ea022443e --- /dev/null +++ b/src/core/shared/result.ts @@ -0,0 +1,37 @@ +export type RepoResult = + | { + ok: true + data: T + error?: undefined + } + | { + ok: false + data?: undefined + error: E + } + +export type RepoErrorCode = + | 'unauthorized' + | 'forbidden' + | 'not_found' + | 'validation' + | 'conflict' + | 'internal' + | 'unavailable' + +export type RepoError = { + code: RepoErrorCode + status: number + message: string + cause?: unknown +} + +export const ok = (data: T): RepoResult => ({ + ok: true, + data, +}) + +export const err = (error: E): RepoResult => ({ + ok: false, + error, +}) diff --git a/src/core/shared/schemas.ts b/src/core/shared/schemas.ts new file mode 100644 index 000000000..5a9678f2f --- /dev/null +++ b/src/core/shared/schemas.ts @@ -0,0 +1,3 @@ +import { z } from 'zod' + +export const NonEmptyStringSchema = z.string().trim().min(1) diff --git a/src/features/auth/oauth-provider-buttons.tsx b/src/features/auth/oauth-provider-buttons.tsx index b6f4e4896..a7fd7855d 100644 --- a/src/features/auth/oauth-provider-buttons.tsx +++ b/src/features/auth/oauth-provider-buttons.tsx @@ -2,7 +2,7 @@ import { useSearchParams } from 'next/navigation' import { useAction } from 'next-safe-action/hooks' -import { signInWithOAuthAction } from '@/server/auth/auth-actions' +import { signInWithOAuthAction } from '@/core/server/actions/auth-actions' import { Button } from '@/ui/primitives/button' export function OAuthProviders() { diff --git a/src/features/dashboard/account/email-settings.tsx b/src/features/dashboard/account/email-settings.tsx index 0bf3bee9c..af10e9441 100644 --- a/src/features/dashboard/account/email-settings.tsx +++ b/src/features/dashboard/account/email-settings.tsx @@ -7,6 +7,7 @@ import { useEffect, useMemo } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { USER_MESSAGES } from '@/configs/user-messages' +import { updateUserAction } from '@/core/server/actions/user-actions' import { defaultErrorToast, defaultSuccessToast, @@ -14,7 +15,6 @@ import { } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { getUserProviders } from '@/lib/utils/auth' -import { updateUserAction } from '@/server/user/user-actions' import { Button } from '@/ui/primitives/button' import { Card, diff --git a/src/features/dashboard/account/name-settings.tsx b/src/features/dashboard/account/name-settings.tsx index 92f4818cf..95307e129 100644 --- a/src/features/dashboard/account/name-settings.tsx +++ b/src/features/dashboard/account/name-settings.tsx @@ -5,13 +5,13 @@ import { useAction } from 'next-safe-action/hooks' import { useForm } from 'react-hook-form' import { z } from 'zod' import { USER_MESSAGES } from '@/configs/user-messages' +import { updateUserAction } from '@/core/server/actions/user-actions' import { defaultErrorToast, defaultSuccessToast, useToast, } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' -import { updateUserAction } from '@/server/user/user-actions' import { Button } from '@/ui/primitives/button' import { Card, diff --git a/src/features/dashboard/account/password-settings.tsx b/src/features/dashboard/account/password-settings.tsx index 4b54f6943..8884ab479 100644 --- a/src/features/dashboard/account/password-settings.tsx +++ b/src/features/dashboard/account/password-settings.tsx @@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { USER_MESSAGES } from '@/configs/user-messages' +import { updateUserAction } from '@/core/server/actions/user-actions' import { defaultErrorToast, defaultSuccessToast, @@ -13,7 +14,6 @@ import { } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { getUserProviders } from '@/lib/utils/auth' -import { updateUserAction } from '@/server/user/user-actions' import { Button } from '@/ui/primitives/button' import { Card, diff --git a/src/features/dashboard/account/reauth-dialog.tsx b/src/features/dashboard/account/reauth-dialog.tsx index 810114dc0..37c0a0f4e 100644 --- a/src/features/dashboard/account/reauth-dialog.tsx +++ b/src/features/dashboard/account/reauth-dialog.tsx @@ -1,7 +1,7 @@ 'use client' import { PROTECTED_URLS } from '@/configs/urls' -import { signOutAction } from '@/server/auth/auth-actions' +import { signOutAction } from '@/core/server/actions/auth-actions' import { AlertDialog } from '@/ui/alert-dialog' interface ReauthDialogProps { diff --git a/src/features/dashboard/account/user-access-token.tsx b/src/features/dashboard/account/user-access-token.tsx index 159c0d405..e95070e22 100644 --- a/src/features/dashboard/account/user-access-token.tsx +++ b/src/features/dashboard/account/user-access-token.tsx @@ -3,8 +3,8 @@ import { Eye, EyeOff } from 'lucide-react' import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' +import { getUserAccessTokenAction } from '@/core/server/actions/user-actions' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' -import { getUserAccessTokenAction } from '@/server/user/user-actions' import CopyButton from '@/ui/copy-button' import { Button } from '@/ui/primitives/button' import { Input } from '@/ui/primitives/input' diff --git a/src/features/dashboard/billing/selected-plan.tsx b/src/features/dashboard/billing/selected-plan.tsx index 4dba6cde9..aa767facc 100644 --- a/src/features/dashboard/billing/selected-plan.tsx +++ b/src/features/dashboard/billing/selected-plan.tsx @@ -4,10 +4,10 @@ import { useMutation } from '@tanstack/react-query' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { PROTECTED_URLS } from '@/configs/urls' +import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' -import type { TeamLimits } from '@/server/team/get-team-limits' import { useTRPC } from '@/trpc/client' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/billing/types.ts b/src/features/dashboard/billing/types.ts index e94d5092d..1df167ddb 100644 --- a/src/features/dashboard/billing/types.ts +++ b/src/features/dashboard/billing/types.ts @@ -1,4 +1,4 @@ -import type { TeamLimits } from '@/server/team/get-team-limits' +import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' import type { TeamItems } from '@/types/billing.types' export interface BillingData { diff --git a/src/features/dashboard/build/build-logs-store.ts b/src/features/dashboard/build/build-logs-store.ts index a479ab184..763ec0f97 100644 --- a/src/features/dashboard/build/build-logs-store.ts +++ b/src/features/dashboard/build/build-logs-store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' -import type { BuildLogDTO } from '@/server/api/models/builds.models' +import type { BuildLogModel } from '@/core/domains/builds/models' import type { useTRPCClient } from '@/trpc/client' import { countLeadingAtTimestamp, @@ -23,7 +23,7 @@ interface BuildLogsParams { type TRPCClient = ReturnType interface BuildLogsState { - logs: BuildLogDTO[] + logs: BuildLogModel[] hasMoreBackwards: boolean isLoadingBackwards: boolean isLoadingForwards: boolean diff --git a/src/features/dashboard/build/header.tsx b/src/features/dashboard/build/header.tsx index d40979ecc..cb5c4ecc9 100644 --- a/src/features/dashboard/build/header.tsx +++ b/src/features/dashboard/build/header.tsx @@ -1,7 +1,7 @@ 'use client' +import type { BuildDetailsModel } from '@/core/domains/builds/models' import { cn } from '@/lib/utils/ui' -import type { BuildDetailsDTO } from '@/server/api/models/builds.models' import CopyButton from '@/ui/copy-button' import CopyButtonInline from '@/ui/copy-button-inline' import { CheckIcon, CloseIcon } from '@/ui/primitives/icons' @@ -11,7 +11,7 @@ import { DetailsItem, DetailsRow } from '../layouts/details-row' import { RanFor, StartedAt, Template } from './header-cells' interface BuildHeaderProps { - buildDetails: BuildDetailsDTO | undefined + buildDetails: BuildDetailsModel | undefined buildId: string templateId: string } @@ -74,8 +74,8 @@ export default function BuildHeader({ } interface StatusBannerProps { - status: BuildDetailsDTO['status'] | undefined - statusMessage?: BuildDetailsDTO['statusMessage'] + status: BuildDetailsModel['status'] | undefined + statusMessage?: BuildDetailsModel['statusMessage'] } function StatusBanner({ status, statusMessage }: StatusBannerProps) { diff --git a/src/features/dashboard/build/logs-cells.tsx b/src/features/dashboard/build/logs-cells.tsx index b42f9e47a..f8a46afc2 100644 --- a/src/features/dashboard/build/logs-cells.tsx +++ b/src/features/dashboard/build/logs-cells.tsx @@ -1,15 +1,15 @@ import { format } from 'date-fns' import { enUS } from 'date-fns/locale/en-US' +import type { BuildLogModel } from '@/core/domains/builds/models' import { LogLevelBadge, LogMessage, } from '@/features/dashboard/common/log-cells' import { formatDurationCompact } from '@/lib/utils/formatting' -import type { BuildLogDTO } from '@/server/api/models/builds.models' import CopyButtonInline from '@/ui/copy-button-inline' import { Badge, type BadgeProps } from '@/ui/primitives/badge' -export const LogLevel = ({ level }: { level: BuildLogDTO['level'] }) => { +export const LogLevel = ({ level }: { level: BuildLogModel['level'] }) => { return } @@ -40,7 +40,7 @@ export const Timestamp = ({ } interface MessageProps { - message: BuildLogDTO['message'] + message: BuildLogModel['message'] } export const Message = ({ message }: MessageProps) => { diff --git a/src/features/dashboard/build/logs-filter-params.ts b/src/features/dashboard/build/logs-filter-params.ts index d972fc419..61edab567 100644 --- a/src/features/dashboard/build/logs-filter-params.ts +++ b/src/features/dashboard/build/logs-filter-params.ts @@ -1,7 +1,7 @@ import { createLoader, parseAsStringEnum } from 'nuqs/server' -import type { BuildLogDTO } from '@/server/api/models/builds.models' +import type { BuildLogModel } from '@/core/domains/builds/models' -export type LogLevelFilter = BuildLogDTO['level'] +export type LogLevelFilter = BuildLogModel['level'] export const LOG_LEVELS: LogLevelFilter[] = ['debug', 'info', 'warn', 'error'] diff --git a/src/features/dashboard/build/logs.tsx b/src/features/dashboard/build/logs.tsx index 7843380cd..934741f4b 100644 --- a/src/features/dashboard/build/logs.tsx +++ b/src/features/dashboard/build/logs.tsx @@ -6,6 +6,10 @@ import { type Virtualizer, } from '@tanstack/react-virtual' import { type RefObject, useCallback, useEffect, useRef, useState } from 'react' +import type { + BuildDetailsModel, + BuildLogModel, +} from '@/core/domains/builds/models' import { LOG_LEVEL_LEFT_BORDER_CLASS, type LogLevelValue, @@ -19,10 +23,6 @@ import { LogVirtualRow, } from '@/features/dashboard/common/log-viewer-ui' import { cn } from '@/lib/utils' -import type { - BuildDetailsDTO, - BuildLogDTO, -} from '@/server/api/models/builds.models' import { Loader } from '@/ui/primitives/loader' import { Table, TableBody, TableCell } from '@/ui/primitives/table' import { LOG_RETENTION_MS } from '../templates/builds/constants' @@ -39,7 +39,7 @@ const VIRTUAL_OVERSCAN = 16 const SCROLL_LOAD_THRESHOLD_PX = 200 interface LogsProps { - buildDetails: BuildDetailsDTO | undefined + buildDetails: BuildDetailsModel | undefined teamIdOrSlug: string templateId: string buildId: string @@ -90,7 +90,7 @@ export default function Logs({ } interface LogsContentProps { - buildDetails: BuildDetailsDTO + buildDetails: BuildDetailsModel teamIdOrSlug: string templateId: string buildId: string @@ -107,7 +107,7 @@ function LogsContent({ setLevel, }: LogsContentProps) { const scrollContainerRef = useRef(null) - const [lastNonEmptyLogs, setLastNonEmptyLogs] = useState([]) + const [lastNonEmptyLogs, setLastNonEmptyLogs] = useState([]) const { isRefetchingFromFilterChange, onFetchComplete } = useFilterRefetchTracking(level) @@ -232,7 +232,7 @@ function EmptyBody({ hasRetainedLogs }: EmptyBodyProps) { } interface VirtualizedLogsBodyProps { - logs: BuildLogDTO[] + logs: BuildLogModel[] scrollContainerRef: RefObject startedAt: number onLoadMore: () => void @@ -497,7 +497,7 @@ function useAutoScrollToBottom({ } interface LogRowProps { - log: BuildLogDTO + log: BuildLogModel isZebraRow: boolean virtualRow: VirtualItem virtualizer: Virtualizer diff --git a/src/features/dashboard/build/use-build-logs.ts b/src/features/dashboard/build/use-build-logs.ts index a1499bf3f..9f6316894 100644 --- a/src/features/dashboard/build/use-build-logs.ts +++ b/src/features/dashboard/build/use-build-logs.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useRef } from 'react' import { useStore } from 'zustand' -import type { BuildStatus } from '@/server/api/models/builds.models' +import type { BuildStatus } from '@/core/domains/builds/models' import { useTRPCClient } from '@/trpc/client' import { type BuildLogsStore, createBuildLogsStore } from './build-logs-store' import type { LogLevelFilter } from './logs-filter-params' diff --git a/src/features/dashboard/members/add-member-form.tsx b/src/features/dashboard/members/add-member-form.tsx index e73ff6d6c..8d38b381b 100644 --- a/src/features/dashboard/members/add-member-form.tsx +++ b/src/features/dashboard/members/add-member-form.tsx @@ -4,13 +4,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useAction } from 'next-safe-action/hooks' import { useForm } from 'react-hook-form' import { z } from 'zod' +import { addTeamMemberAction } from '@/core/server/actions/team-actions' import { defaultErrorToast, defaultSuccessToast, useToast, } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' -import { addTeamMemberAction } from '@/server/team/team-actions' import { Button } from '@/ui/primitives/button' import { Form, diff --git a/src/features/dashboard/members/danger-zone.tsx b/src/features/dashboard/members/danger-zone.tsx index e42e5ce2c..4036a8af5 100644 --- a/src/features/dashboard/members/danger-zone.tsx +++ b/src/features/dashboard/members/danger-zone.tsx @@ -1,4 +1,4 @@ -import { getTeam } from '@/server/team/get-team' +import { getTeam } from '@/core/server/functions/team/get-team' import { AlertDialog } from '@/ui/alert-dialog' import ErrorBoundary from '@/ui/error' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/members/member-table-body.tsx b/src/features/dashboard/members/member-table-body.tsx index 4eace9b97..aee662333 100644 --- a/src/features/dashboard/members/member-table-body.tsx +++ b/src/features/dashboard/members/member-table-body.tsx @@ -1,4 +1,4 @@ -import { getTeamMembers } from '@/server/team/get-team-members' +import { getTeamMembers } from '@/core/server/functions/team/get-team-members' import { ErrorIndicator } from '@/ui/error-indicator' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' import { TableCell, TableRow } from '@/ui/primitives/table' diff --git a/src/features/dashboard/members/member-table-row.tsx b/src/features/dashboard/members/member-table-row.tsx index 83782682a..f85feff08 100644 --- a/src/features/dashboard/members/member-table-row.tsx +++ b/src/features/dashboard/members/member-table-row.tsx @@ -4,13 +4,13 @@ import { useRouter } from 'next/navigation' import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import { removeTeamMemberAction } from '@/core/server/actions/team-actions' +import type { TeamMember } from '@/core/server/functions/team/types' import { defaultErrorToast, defaultSuccessToast, useToast, } from '@/lib/hooks/use-toast' -import { removeTeamMemberAction } from '@/server/team/team-actions' -import type { TeamMember } from '@/server/team/types' import { AlertDialog } from '@/ui/alert-dialog' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index ad15487b5..a4f41a3ab 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -4,13 +4,13 @@ import { useQuery } from '@tanstack/react-query' import type { ReactNode } from 'react' import { createContext, useCallback, useContext, useMemo } from 'react' import { SANDBOXES_METRICS_POLLING_MS } from '@/configs/intervals' +import type { + SandboxDetailsModel, + SandboxEventModel, +} from '@/core/domains/sandboxes/models' import { useAlignedRefetchInterval } from '@/lib/hooks/use-aligned-refetch-interval' import { useRouteParams } from '@/lib/hooks/use-route-params' import { isNotFoundError } from '@/lib/utils/trpc-errors' -import type { - SandboxDetailsDTO, - SandboxEventDTO, -} from '@/server/api/models/sandboxes.models' import { useTRPC } from '@/trpc/client' import { SANDBOX_LIFECYCLE_EVENT_KILLED } from './monitoring/utils/constants' @@ -18,11 +18,11 @@ export interface SandboxLifecycleState { createdAt: string | null pausedAt: string | null endedAt: string | null - events: SandboxEventDTO[] + events: SandboxEventModel[] } interface SandboxContextValue { - sandboxInfo?: SandboxDetailsDTO + sandboxInfo?: SandboxDetailsModel sandboxLifecycle: SandboxLifecycleState | null isRunning: boolean isSandboxNotFound: boolean @@ -46,7 +46,7 @@ interface SandboxProviderProps { } function buildSandboxLifecycle( - sandboxInfo: SandboxDetailsDTO | undefined + sandboxInfo: SandboxDetailsModel | undefined ): SandboxLifecycleState | null { if (!sandboxInfo) { return null @@ -88,7 +88,9 @@ export function SandboxProvider({ children }: SandboxProviderProps) { { retry: false, refetchInterval: (query) => { - const sandboxInfo = query.state.data as SandboxDetailsDTO | undefined + const sandboxInfo = query.state.data as + | SandboxDetailsModel + | undefined const state = sandboxInfo?.state // Keep polling when killed but the killed lifecycle event hasn't diff --git a/src/features/dashboard/sandbox/header/kill-button.tsx b/src/features/dashboard/sandbox/header/kill-button.tsx index 5332d5dbb..cae881b08 100644 --- a/src/features/dashboard/sandbox/header/kill-button.tsx +++ b/src/features/dashboard/sandbox/header/kill-button.tsx @@ -3,8 +3,8 @@ import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' import { toast } from 'sonner' +import { killSandboxAction } from '@/core/server/actions/sandbox-actions' import { cn } from '@/lib/utils/ui' -import { killSandboxAction } from '@/server/sandboxes/sandbox-actions' import { AlertPopover } from '@/ui/alert-popover' import { Button } from '@/ui/primitives/button' import { TrashIcon } from '@/ui/primitives/icons' diff --git a/src/features/dashboard/sandbox/logs/logs-cells.tsx b/src/features/dashboard/sandbox/logs/logs-cells.tsx index 29412fd9d..f5a2739bf 100644 --- a/src/features/dashboard/sandbox/logs/logs-cells.tsx +++ b/src/features/dashboard/sandbox/logs/logs-cells.tsx @@ -1,5 +1,5 @@ +import type { SandboxLogModel } from '@/core/domains/sandboxes/models' import { LogLevelBadge } from '@/features/dashboard/common/log-cells' -import type { SandboxLogDTO } from '@/server/api/models/sandboxes.models' import CopyButtonInline from '@/ui/copy-button-inline' const LOCAL_DATE_FORMATTER = new Intl.DateTimeFormat(undefined, { @@ -14,7 +14,7 @@ const LOCAL_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour12: false, }) -export const LogLevel = ({ level }: { level: SandboxLogDTO['level'] }) => { +export const LogLevel = ({ level }: { level: SandboxLogModel['level'] }) => { return } @@ -43,7 +43,7 @@ export const Timestamp = ({ timestampUnix }: TimestampProps) => { } interface MessageProps { - message: SandboxLogDTO['message'] + message: SandboxLogModel['message'] search: string shouldHighlight: boolean } diff --git a/src/features/dashboard/sandbox/logs/logs-filter-params.ts b/src/features/dashboard/sandbox/logs/logs-filter-params.ts index 912decd32..8aa4215f6 100644 --- a/src/features/dashboard/sandbox/logs/logs-filter-params.ts +++ b/src/features/dashboard/sandbox/logs/logs-filter-params.ts @@ -1,7 +1,7 @@ import { createLoader, parseAsString, parseAsStringEnum } from 'nuqs/server' -import type { SandboxLogDTO } from '@/server/api/models/sandboxes.models' +import type { SandboxLogModel } from '@/core/domains/sandboxes/models' -export type LogLevelFilter = SandboxLogDTO['level'] +export type LogLevelFilter = SandboxLogModel['level'] export const LOG_LEVELS: LogLevelFilter[] = ['debug', 'info', 'warn', 'error'] diff --git a/src/features/dashboard/sandbox/logs/logs.tsx b/src/features/dashboard/sandbox/logs/logs.tsx index f53d34064..163d4b84c 100644 --- a/src/features/dashboard/sandbox/logs/logs.tsx +++ b/src/features/dashboard/sandbox/logs/logs.tsx @@ -13,6 +13,7 @@ import { useState, } from 'react' import { LOG_RETENTION_MS } from '@/configs/logs' +import type { SandboxLogModel } from '@/core/domains/sandboxes/models' import { LOG_LEVEL_LEFT_BORDER_CLASS, type LogLevelValue, @@ -26,7 +27,6 @@ import { LogVirtualRow, } from '@/features/dashboard/common/log-viewer-ui' import { cn } from '@/lib/utils' -import type { SandboxLogDTO } from '@/server/api/models/sandboxes.models' import { DebouncedInput } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader' import { Table, TableBody, TableCell } from '@/ui/primitives/table' @@ -129,7 +129,9 @@ function LogsContent({ }: LogsContentProps) { const [scrollContainerElement, setScrollContainerElement] = useState(null) - const [lastNonEmptyLogs, setLastNonEmptyLogs] = useState([]) + const [lastNonEmptyLogs, setLastNonEmptyLogs] = useState( + [] + ) const { logs, @@ -309,7 +311,7 @@ function FiltersRow({ } interface VirtualizedLogsBodyProps { - logs: SandboxLogDTO[] + logs: SandboxLogModel[] scrollContainerElement: HTMLDivElement onLoadMore: () => void hasNextPage: boolean @@ -609,7 +611,7 @@ function useAutoScrollToBottom({ } interface LogRowProps { - log: SandboxLogDTO + log: SandboxLogModel search: string shouldHighlight: boolean isZebraRow: boolean diff --git a/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts b/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts index 50a430229..8028885ee 100644 --- a/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts +++ b/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' -import type { SandboxLogDTO } from '@/server/api/models/sandboxes.models' +import type { SandboxLogModel } from '@/core/domains/sandboxes/models' import type { useTRPCClient } from '@/trpc/client' import { countLeadingAtTimestamp, @@ -20,7 +20,7 @@ interface SandboxLogsParams { type TRPCClient = ReturnType interface SandboxLogsState { - logs: SandboxLogDTO[] + logs: SandboxLogModel[] hasMoreBackwards: boolean isLoadingBackwards: boolean isLoadingForwards: boolean diff --git a/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts b/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts index f2014d024..3a1cc521b 100644 --- a/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts +++ b/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts @@ -3,10 +3,10 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query' import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs' import { useCallback, useEffect, useMemo, useRef } from 'react' +import type { SandboxMetric } from '@/core/domains/sandboxes/models' import { useDashboard } from '@/features/dashboard/context' import { useSandboxContext } from '@/features/dashboard/sandbox/context' import { getMsUntilNextAlignedInterval } from '@/lib/hooks/use-aligned-refetch-interval' -import type { SandboxMetric } from '@/server/api/models/sandboxes.models' import { useTRPCClient } from '@/trpc/client' import { SANDBOX_LIFECYCLE_EVENT_KILLED, diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts index 6af90b6e8..e99b48f5e 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts @@ -1,4 +1,4 @@ -import type { SandboxEventDTO } from '@/server/api/models/sandboxes.models' +import type { SandboxEventModel } from '@/core/domains/sandboxes/models' import type { SandboxMetricsDataPoint, SandboxMetricsLifecycleEventMarker, @@ -46,8 +46,8 @@ const EVENT_STYLES: Record = { } function sortLifecycleEventsByTimestamp( - events: SandboxEventDTO[] -): SandboxEventDTO[] { + events: SandboxEventModel[] +): SandboxEventModel[] { return [...events].sort((a, b) => { const timestampA = parseDateTimestampMs(a.timestamp) ?? Number.MAX_SAFE_INTEGER @@ -106,7 +106,7 @@ function toVisiblePauseWindow( } export function buildInactiveWindows( - lifecycleEvents: SandboxEventDTO[], + lifecycleEvents: SandboxEventModel[], rangeStart: number, rangeEnd: number ): LifecyclePauseWindow[] { @@ -217,7 +217,7 @@ export function buildInactiveWindows( } export function buildLifecycleEventMarkers( - lifecycleEvents: SandboxEventDTO[], + lifecycleEvents: SandboxEventModel[], rangeStart: number, rangeEnd: number ): SandboxMetricsLifecycleEventMarker[] { diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts index 91f7e4fc5..69f762a6c 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts @@ -1,5 +1,5 @@ import { millisecondsInSecond } from 'date-fns/constants' -import type { SandboxMetric } from '@/server/api/models/sandboxes.models' +import type { SandboxMetric } from '@/core/domains/sandboxes/models' import type { SandboxMetricsDataPoint, SandboxMetricsSeries, diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts index 9370c3cc7..3d6590770 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts @@ -1,7 +1,7 @@ import type { - SandboxEventDTO, + SandboxEventModel, SandboxMetric, -} from '@/server/api/models/sandboxes.models' +} from '@/core/domains/sandboxes/models' import type { MonitoringChartModel } from '../types/sandbox-metrics-chart' import { applyPauseWindows, @@ -18,7 +18,7 @@ import { interface BuildMonitoringChartModelOptions { metrics: SandboxMetric[] - lifecycleEvents?: SandboxEventDTO[] + lifecycleEvents?: SandboxEventModel[] startMs: number endMs: number } diff --git a/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts b/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts index 6e9b7bc17..57aaa11f7 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts @@ -1,5 +1,5 @@ +import type { SandboxDetailsModel } from '@/core/domains/sandboxes/models' import { calculateStepForDuration } from '@/features/dashboard/sandboxes/monitoring/utils' -import type { SandboxDetailsDTO } from '@/server/api/models/sandboxes.models' import { SANDBOX_MONITORING_DEFAULT_RANGE_MS, SANDBOX_MONITORING_LIFECYCLE_PADDING_STEPS, @@ -150,7 +150,7 @@ export interface SandboxLifecycleBounds { } export function getSandboxLifecycleBounds( - sandboxLifecycle: Pick & { + sandboxLifecycle: Pick & { createdAt?: string | null pausedAt?: string | null endedAt?: string | null diff --git a/src/features/dashboard/sandboxes/live-counter.client.tsx b/src/features/dashboard/sandboxes/live-counter.client.tsx index c35f0d658..e49977db2 100644 --- a/src/features/dashboard/sandboxes/live-counter.client.tsx +++ b/src/features/dashboard/sandboxes/live-counter.client.tsx @@ -2,7 +2,7 @@ import type { InferSafeActionFnResult } from 'next-safe-action' import type { NonUndefined } from 'react-hook-form' -import type { getTeamMetrics } from '@/server/sandboxes/get-team-metrics' +import type { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' import { LiveSandboxCounter } from './live-counter' import { useRecentMetrics } from './monitoring/hooks/use-recent-metrics' diff --git a/src/features/dashboard/sandboxes/live-counter.server.tsx b/src/features/dashboard/sandboxes/live-counter.server.tsx index 5d0c6bd20..86827a0ef 100644 --- a/src/features/dashboard/sandboxes/live-counter.server.tsx +++ b/src/features/dashboard/sandboxes/live-counter.server.tsx @@ -1,8 +1,8 @@ import { Suspense } from 'react' +import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' import { l } from '@/lib/clients/logger/logger' import { cn } from '@/lib/utils' import { getNowMemo } from '@/lib/utils/server' -import { getTeamMetrics } from '@/server/sandboxes/get-team-metrics' import { Skeleton } from '@/ui/primitives/skeleton' import { LiveSandboxCounterClient } from './live-counter.client' diff --git a/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx b/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx index a858d5ede..034baf895 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx +++ b/src/features/dashboard/sandboxes/monitoring/charts/charts.tsx @@ -1,7 +1,7 @@ import { Suspense } from 'react' import { TEAM_METRICS_INITIAL_RANGE_MS } from '@/configs/intervals' -import { getTeamMetrics } from '@/server/sandboxes/get-team-metrics' -import { getTeamLimits } from '@/server/team/get-team-limits' +import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' +import { getTeamLimits } from '@/core/server/functions/team/get-team-limits' import { TeamMetricsChartsProvider } from '../charts-context' import ConcurrentChartClient from './concurrent-chart' import ChartFallback from './fallback' diff --git a/src/features/dashboard/sandboxes/monitoring/header.client.tsx b/src/features/dashboard/sandboxes/monitoring/header.client.tsx index 098ec85e3..72a178897 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.client.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.client.tsx @@ -3,8 +3,8 @@ import type { InferSafeActionFnResult } from 'next-safe-action' import { useMemo } from 'react' import type { NonUndefined } from 'react-hook-form' +import type { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' import { formatDecimal, formatNumber } from '@/lib/utils/formatting' -import type { getTeamMetrics } from '@/server/sandboxes/get-team-metrics' import { AnimatedNumber } from '@/ui/primitives/animated-number' import { useRecentMetrics } from './hooks/use-recent-metrics' diff --git a/src/features/dashboard/sandboxes/monitoring/header.tsx b/src/features/dashboard/sandboxes/monitoring/header.tsx index 295b9efe3..2a23eaf1e 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.tsx @@ -1,10 +1,10 @@ import { AlertTriangle } from 'lucide-react' import { Suspense } from 'react' +import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' +import { getTeamMetricsMax } from '@/core/server/functions/sandboxes/get-team-metrics-max' +import { getTeamLimits } from '@/core/server/functions/team/get-team-limits' import { formatNumber } from '@/lib/utils/formatting' import { getNowMemo } from '@/lib/utils/server' -import { getTeamMetrics } from '@/server/sandboxes/get-team-metrics' -import { getTeamMetricsMax } from '@/server/sandboxes/get-team-metrics-max' -import { getTeamLimits } from '@/server/team/get-team-limits' import ErrorTooltip from '@/ui/error-tooltip' import { SemiLiveBadge } from '@/ui/live' import { Skeleton } from '@/ui/primitives/skeleton' diff --git a/src/features/dashboard/settings/general/name-card.tsx b/src/features/dashboard/settings/general/name-card.tsx index 60219b59a..1dc139e63 100644 --- a/src/features/dashboard/settings/general/name-card.tsx +++ b/src/features/dashboard/settings/general/name-card.tsx @@ -4,6 +4,8 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormOptimisticAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { AnimatePresence, motion } from 'motion/react' import { USER_MESSAGES } from '@/configs/user-messages' +import { updateTeamNameAction } from '@/core/server/actions/team-actions' +import { UpdateTeamNameSchema } from '@/core/server/functions/team/types' import { useDashboard } from '@/features/dashboard/context' import { defaultErrorToast, @@ -11,8 +13,6 @@ import { useToast, } from '@/lib/hooks/use-toast' import { exponentialSmoothing } from '@/lib/utils' -import { updateTeamNameAction } from '@/server/team/team-actions' -import { UpdateTeamNameSchema } from '@/server/team/types' import { Button } from '@/ui/primitives/button' import { Card, diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index 3505f1c41..cd1f41829 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -5,6 +5,7 @@ import { ChevronsUp, ImagePlusIcon, Loader2, Pencil } from 'lucide-react' import { useAction } from 'next-safe-action/hooks' import { useRef, useState } from 'react' import { USER_MESSAGES } from '@/configs/user-messages' +import { uploadTeamProfilePictureAction } from '@/core/server/actions/team-actions' import { useDashboard } from '@/features/dashboard/context' import { defaultErrorToast, @@ -12,7 +13,6 @@ import { useToast, } from '@/lib/hooks/use-toast' import { cn, exponentialSmoothing } from '@/lib/utils' -import { uploadTeamProfilePictureAction } from '@/server/team/team-actions' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { Badge } from '@/ui/primitives/badge' import { cardVariants } from '@/ui/primitives/card' diff --git a/src/features/dashboard/settings/keys/create-api-key-dialog.tsx b/src/features/dashboard/settings/keys/create-api-key-dialog.tsx index 27b83b745..a33457f3b 100644 --- a/src/features/dashboard/settings/keys/create-api-key-dialog.tsx +++ b/src/features/dashboard/settings/keys/create-api-key-dialog.tsx @@ -7,8 +7,8 @@ import { usePostHog } from 'posthog-js/react' import { type FC, type ReactNode, useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' +import { createApiKeyAction } from '@/core/server/actions/key-actions' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' -import { createApiKeyAction } from '@/server/keys/key-actions' import CopyButton from '@/ui/copy-button' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/settings/keys/table-body.tsx b/src/features/dashboard/settings/keys/table-body.tsx index 2a7a0f706..3a2b6a3d1 100644 --- a/src/features/dashboard/settings/keys/table-body.tsx +++ b/src/features/dashboard/settings/keys/table-body.tsx @@ -1,5 +1,5 @@ import { CLI_GENERATED_KEY_NAME } from '@/configs/api' -import { getTeamApiKeys } from '@/server/keys/get-api-keys' +import { getTeamApiKeys } from '@/core/server/functions/keys/get-api-keys' import { ErrorIndicator } from '@/ui/error-indicator' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' import { TableCell, TableRow } from '@/ui/primitives/table' diff --git a/src/features/dashboard/settings/keys/table-row.tsx b/src/features/dashboard/settings/keys/table-row.tsx index fa81952ee..fd650f611 100644 --- a/src/features/dashboard/settings/keys/table-row.tsx +++ b/src/features/dashboard/settings/keys/table-row.tsx @@ -5,6 +5,7 @@ import { motion } from 'motion/react' import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' import { API_KEYS_LAST_USED_FIRST_COLLECTION_DATE } from '@/configs/versioning' +import { deleteApiKeyAction } from '@/core/server/actions/key-actions' import { useDashboard } from '@/features/dashboard/context' import { defaultErrorToast, @@ -12,7 +13,6 @@ import { useToast, } from '@/lib/hooks/use-toast' import { exponentialSmoothing } from '@/lib/utils' -import { deleteApiKeyAction } from '@/server/keys/key-actions' import type { TeamAPIKey } from '@/types/api.types' import { AlertDialog } from '@/ui/alert-dialog' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx index 96f129ba6..a4620b702 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react' import type { UseFormReturn } from 'react-hook-form' import ShikiHighlighter from 'react-shiki' import { useShikiTheme } from '@/configs/shiki' -import type { UpsertWebhookSchemaType } from '@/server/webhooks/schema' +import type { UpsertWebhookSchemaType } from '@/core/server/functions/webhooks/schema' import { Button } from '@/ui/primitives/button' import { Checkbox } from '@/ui/primitives/checkbox' import { diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 0c40d6de3..a8a0438f2 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -4,13 +4,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { PlusIcon } from 'lucide-react' import { useState } from 'react' +import { upsertWebhookAction } from '@/core/server/actions/webhooks-actions' +import { UpsertWebhookSchema } from '@/core/server/functions/webhooks/schema' import { defaultErrorToast, defaultSuccessToast, toast, } from '@/lib/hooks/use-toast' -import { UpsertWebhookSchema } from '@/server/webhooks/schema' -import { upsertWebhookAction } from '@/server/webhooks/webhooks-actions' import { Button } from '@/ui/primitives/button' import { Dialog, @@ -153,7 +153,7 @@ export default function WebhookAddEditDialog({ if (currentEvents.includes(event)) { form.setValue( 'events', - currentEvents.filter((e) => e !== event) + currentEvents.filter((eventName: string) => eventName !== event) ) } else { form.setValue('events', [...currentEvents, event]) diff --git a/src/features/dashboard/settings/webhooks/delete-dialog.tsx b/src/features/dashboard/settings/webhooks/delete-dialog.tsx index eb85a43b3..39397560b 100644 --- a/src/features/dashboard/settings/webhooks/delete-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/delete-dialog.tsx @@ -2,12 +2,12 @@ import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' +import { deleteWebhookAction } from '@/core/server/actions/webhooks-actions' import { defaultErrorToast, defaultSuccessToast, useToast, } from '@/lib/hooks/use-toast' -import { deleteWebhookAction } from '@/server/webhooks/webhooks-actions' import { AlertDialog } from '@/ui/alert-dialog' import { TrashIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' diff --git a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx index a900f3461..52fc1d56e 100644 --- a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx @@ -3,13 +3,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { useState } from 'react' +import { updateWebhookSecretAction } from '@/core/server/actions/webhooks-actions' +import { UpdateWebhookSecretSchema } from '@/core/server/functions/webhooks/schema' import { defaultErrorToast, defaultSuccessToast, toast, } from '@/lib/hooks/use-toast' -import { UpdateWebhookSecretSchema } from '@/server/webhooks/schema' -import { updateWebhookSecretAction } from '@/server/webhooks/webhooks-actions' import { Button } from '@/ui/primitives/button' import { Dialog, diff --git a/src/features/dashboard/settings/webhooks/table-body.tsx b/src/features/dashboard/settings/webhooks/table-body.tsx index 9498c3446..c64b86959 100644 --- a/src/features/dashboard/settings/webhooks/table-body.tsx +++ b/src/features/dashboard/settings/webhooks/table-body.tsx @@ -1,4 +1,4 @@ -import { getWebhooks } from '@/server/webhooks/get-webhooks' +import { getWebhooks } from '@/core/server/functions/webhooks/get-webhooks' import { TableCell, TableRow } from '@/ui/primitives/table' import WebhooksEmpty from './empty' import WebhookTableRow from './table-row' diff --git a/src/features/dashboard/sidebar/create-team-dialog.tsx b/src/features/dashboard/sidebar/create-team-dialog.tsx index 5ba1759d7..0efc84d3f 100644 --- a/src/features/dashboard/sidebar/create-team-dialog.tsx +++ b/src/features/dashboard/sidebar/create-team-dialog.tsx @@ -4,13 +4,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { useRouter } from 'next/navigation' import { PROTECTED_URLS } from '@/configs/urls' +import { createTeamAction } from '@/core/server/actions/team-actions' +import { CreateTeamSchema } from '@/core/server/functions/team/types' import { defaultErrorToast, defaultSuccessToast, toast, } from '@/lib/hooks/use-toast' -import { createTeamAction } from '@/server/team/team-actions' -import { CreateTeamSchema } from '@/server/team/types' import { Button } from '@/ui/primitives/button' import { Dialog, diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 2c5fa7158..ba2204015 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -4,8 +4,8 @@ import { ChevronsUpDown, LogOut, Plus, UserRoundCog } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import { signOutAction } from '@/core/server/actions/auth-actions' import { cn } from '@/lib/utils' -import { signOutAction } from '@/server/auth/auth-actions' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { DropdownMenu, diff --git a/src/features/dashboard/templates/builds/constants.ts b/src/features/dashboard/templates/builds/constants.ts index 1669ce53d..eb98a9d63 100644 --- a/src/features/dashboard/templates/builds/constants.ts +++ b/src/features/dashboard/templates/builds/constants.ts @@ -1,5 +1,5 @@ import { millisecondsInDay } from 'date-fns/constants' -import type { BuildStatus } from '@/server/api/models/builds.models' +import type { BuildStatus } from '@/core/domains/builds/models' export const LOG_RETENTION_MS = 7 * millisecondsInDay // 7 days diff --git a/src/features/dashboard/templates/builds/header.tsx b/src/features/dashboard/templates/builds/header.tsx index 99f79d676..d6fd344cb 100644 --- a/src/features/dashboard/templates/builds/header.tsx +++ b/src/features/dashboard/templates/builds/header.tsx @@ -1,8 +1,8 @@ 'use client' import { useEffect, useState } from 'react' +import type { BuildStatus } from '@/core/domains/builds/models' import { cn } from '@/lib/utils' -import type { BuildStatus } from '@/server/api/models/builds.models' import { Button } from '@/ui/primitives/button' import { DropdownMenu, diff --git a/src/features/dashboard/templates/builds/table-cells.tsx b/src/features/dashboard/templates/builds/table-cells.tsx index 2d2c814b1..0a86d1302 100644 --- a/src/features/dashboard/templates/builds/table-cells.tsx +++ b/src/features/dashboard/templates/builds/table-cells.tsx @@ -4,6 +4,10 @@ import { ArrowUpRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import type { + BuildStatus, + ListedBuildModel, +} from '@/core/domains/builds/models' import { useTemplateTableStore } from '@/features/dashboard/templates/list/stores/table-store' import { useRouteParams } from '@/lib/hooks/use-route-params' import { cn } from '@/lib/utils' @@ -11,10 +15,6 @@ import { formatDurationCompact, formatTimeAgoCompact, } from '@/lib/utils/formatting' -import type { - BuildStatus, - ListedBuildDTO, -} from '@/server/api/models/builds.models' import CopyButtonInline from '@/ui/copy-button-inline' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' @@ -196,7 +196,7 @@ export function Status({ status }: StatusProps) { export function Reason({ statusMessage, }: { - statusMessage: ListedBuildDTO['statusMessage'] + statusMessage: ListedBuildModel['statusMessage'] }) { if (!statusMessage) return null diff --git a/src/features/dashboard/templates/builds/table.tsx b/src/features/dashboard/templates/builds/table.tsx index 8af706a66..0a6034285 100644 --- a/src/features/dashboard/templates/builds/table.tsx +++ b/src/features/dashboard/templates/builds/table.tsx @@ -9,12 +9,12 @@ import { import { useRouter } from 'next/navigation' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import type { + ListedBuildModel, + RunningBuildStatusModel, +} from '@/core/domains/builds/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { cn } from '@/lib/utils/ui' -import type { - ListedBuildDTO, - RunningBuildStatusDTO, -} from '@/server/api/models/builds.models' import { useTRPC } from '@/trpc/client' import { ArrowDownIcon } from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' @@ -334,9 +334,9 @@ function useFilterChangeTracking( } function mergeBuildsWithLiveStatuses( - builds: ListedBuildDTO[], - runningStatusesData: RunningBuildStatusDTO[] | undefined -): ListedBuildDTO[] { + builds: ListedBuildModel[], + runningStatusesData: RunningBuildStatusModel[] | undefined +): ListedBuildModel[] { if (!runningStatusesData || runningStatusesData.length === 0) return builds const statusMap = new Map(runningStatusesData.map((s) => [s.id, s])) diff --git a/src/features/dashboard/templates/builds/use-filters.tsx b/src/features/dashboard/templates/builds/use-filters.tsx index 26950e3ea..4d0d3fd1e 100644 --- a/src/features/dashboard/templates/builds/use-filters.tsx +++ b/src/features/dashboard/templates/builds/use-filters.tsx @@ -3,7 +3,7 @@ import { useQueryStates } from 'nuqs' import { useMemo } from 'react' import { useDebounceCallback } from 'usehooks-ts' -import type { BuildStatus } from '@/server/api/models/builds.models' +import type { BuildStatus } from '@/core/domains/builds/models' import { INITIAL_BUILD_STATUSES } from './constants' import { templateBuildsFilterParams } from './filter-params' diff --git a/src/lib/clients/action.ts b/src/lib/clients/action.ts index 5723a7a3b..8d5e9912b 100644 --- a/src/lib/clients/action.ts +++ b/src/lib/clients/action.ts @@ -4,9 +4,13 @@ import { unauthorized } from 'next/navigation' import { createMiddleware, createSafeActionClient } from 'next-safe-action' import { serializeError } from 'serialize-error' import { z } from 'zod' -import { getSessionInsecure } from '@/server/auth/get-session' -import getUserByToken from '@/server/auth/get-user-by-token' -import { getTeamIdFromSegment } from '@/server/team/get-team-id-from-segment' +import { + createRequestContext, + type RequestContextServices, +} from '@/core/server/context/request-context' +import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import getUserByToken from '@/core/server/functions/auth/get-user-by-token' +import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' import { UnauthenticatedError, UnknownError } from '@/types/errors' import { ActionError, flattenClientInputValue } from '../utils/action' import { l } from './logger/logger' @@ -118,7 +122,7 @@ export const actionClient = createSafeActionClient({ ...baseLogPayload, error: sE, }, - `${type} ${name} failed in ${baseLogPayload.server_function_duration_ms}ms: ${typeof sE === 'string' ? sE : ((sE.name || sE.code) && `${sE.name || sE.code}: ` + sE.message) || 'Unknown error'}` + `${type} ${name} failed in ${baseLogPayload.server_function_duration_ms}ms: ${typeof sE === 'string' ? sE : ((sE.name || sE.code) && `${sE.name || sE.code}: ${sE.message}`) || 'Unknown error'}` ) } else { s.setStatus({ code: SpanStatusCode.OK }) @@ -164,7 +168,16 @@ export const authActionClient = actionClient.use(async ({ next }) => { throw UnauthenticatedError() } - return next({ ctx: { user, session, supabase } }) + return next({ + ctx: { + user, + session, + supabase, + services: createRequestContext({ + accessToken: session.access_token, + }).services, + }, + }) }) /** @@ -193,6 +206,7 @@ export const withTeamIdResolution = createMiddleware<{ user: User session: Session supabase: Awaited> + services: RequestContextServices } }>().define(async ({ next, clientInput, ctx }) => { if ( @@ -236,6 +250,12 @@ export const withTeamIdResolution = createMiddleware<{ } return next({ - ctx: { teamId }, + ctx: { + teamId, + services: createRequestContext({ + accessToken: ctx.session.access_token, + teamId, + }).services, + }, }) }) diff --git a/src/lib/utils/trpc-errors.ts b/src/lib/utils/trpc-errors.ts index 1ecec3f13..0ef73764b 100644 --- a/src/lib/utils/trpc-errors.ts +++ b/src/lib/utils/trpc-errors.ts @@ -1,5 +1,5 @@ import { TRPCClientError, type TRPCClientErrorLike } from '@trpc/client' -import type { TRPCAppRouter } from '@/server/api/routers' +import type { TRPCAppRouter } from '@/core/server/api/routers' export function isNotFoundError( error: unknown diff --git a/src/proxy.ts b/src/proxy.ts index 64bf11665..ab6b4f841 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,10 +2,10 @@ import { createServerClient } from '@supabase/ssr' import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { ALLOW_SEO_INDEXING } from './configs/flags' +import { getAuthRedirect } from './core/server/http/proxy' import { l } from './lib/clients/logger/logger' import { getMiddlewareRedirectFromPath } from './lib/utils/redirects' import { getRewriteForPath } from './lib/utils/rewrites' -import { getAuthRedirect } from './server/proxy' export async function proxy(request: NextRequest) { try { diff --git a/src/server/api/repositories/auth.repository.ts b/src/server/api/repositories/auth.repository.ts deleted file mode 100644 index 4f1c5182d..000000000 --- a/src/server/api/repositories/auth.repository.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { TRPCError } from '@trpc/server' -import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' -import type { OtpType } from '../models/auth.models' - -interface VerifyOtpResult { - userId: string -} - -/** - * Verifies an OTP token with Supabase Auth. - * Creates a session and sets auth cookies on success. - * @throws TRPCError on verification failure - */ -async function verifyOtp( - tokenHash: string, - type: OtpType -): Promise { - const supabase = await createClient() - - const { data, error } = await supabase.auth.verifyOtp({ - type, - token_hash: tokenHash, - }) - - if (error) { - l.error( - { - key: 'auth_repository:verify_otp:error', - error: serializeError(error), - context: { - type, - token_hash_prefix: tokenHash.slice(0, 10), - error_code: error.code, - error_status: error.status, - }, - }, - `failed to verify OTP: ${error.message}` - ) - - if (error.status === 403 && error.code === 'otp_expired') { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Email link has expired. Please request a new one.', - }) - } - - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Invalid or expired verification link.', - }) - } - - if (!data.user) { - l.error( - { - key: 'auth_repository:verify_otp:no_user', - context: { - type, - token_hash_prefix: tokenHash.slice(0, 10), - }, - }, - `failed to verify OTP: no user found` - ) - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Verification failed. Please try again.', - }) - } - - // verify session was created (cookies should be set by supabase client) - const hasSession = !!data.session - const hasAccessToken = !!data.session?.access_token - const hasRefreshToken = !!data.session?.refresh_token - - l.info( - { - key: 'auth_repository:verify_otp:success', - user_id: data.user.id, - context: { - type, - token_hash_prefix: tokenHash.slice(0, 10), - has_session: !!data.session, - has_access_token: !!data.session?.access_token, - has_refresh_token: !!data.session?.refresh_token, - session_expires_at: data.session?.expires_at, - }, - }, - `verified OTP for user: ${data.user.id}` - ) - - if (!hasSession) { - l.warn( - { - key: 'auth_repository:verify_otp:no_session', - user_id: data.user.id, - context: { type, tokenHashPrefix: tokenHash.slice(0, 10) }, - }, - `OTP verified but no session returned - user may not be signed in` - ) - } - - return { - userId: data.user.id, - } -} - -export const authRepo = { - verifyOtp, -} diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts deleted file mode 100644 index 55a875e22..000000000 --- a/src/server/api/repositories/builds.repository.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { INITIAL_BUILD_STATUSES } from '@/features/dashboard/templates/builds/constants' -import { api, infra } from '@/lib/clients/api' -import { handleDashboardApiError, handleInfraApiError } from '../errors' -import type { - BuildStatus, - ListedBuildDTO, - RunningBuildStatusDTO, -} from '../models/builds.models' - -// helpers - -const LIST_BUILDS_DEFAULT_LIMIT = 50 -const LIST_BUILDS_MIN_LIMIT = 1 -const LIST_BUILDS_MAX_LIMIT = 100 - -function normalizeListBuildsLimit(limit?: number): number { - return Math.max( - LIST_BUILDS_MIN_LIMIT, - Math.min(limit ?? LIST_BUILDS_DEFAULT_LIMIT, LIST_BUILDS_MAX_LIMIT) - ) -} - -// list builds - -interface ListBuildsOptions { - limit?: number - cursor?: string -} - -interface ListBuildsResult { - data: ListedBuildDTO[] - nextCursor: string | null -} - -interface BuildInfoResult { - names: string[] | null - createdAt: number - finishedAt: number | null - status: ListedBuildDTO['status'] - statusMessage: string | null -} - -async function listBuilds( - accessToken: string, - teamId: string, - buildIdOrTemplate?: string, - statuses: BuildStatus[] = INITIAL_BUILD_STATUSES, - options: ListBuildsOptions = {} -): Promise { - const limit = normalizeListBuildsLimit(options.limit) - const result = await api.GET('/builds', { - params: { - query: { - build_id_or_template: buildIdOrTemplate?.trim() || undefined, - statuses, - limit, - cursor: options.cursor, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, - error: result.error, - teamId, - path: '/builds', - logKey: 'repositories:builds:list_builds:dashboard_api_error', - }) - } - - const builds = result.data?.data ?? [] - if (builds.length === 0) { - return { - data: [], - nextCursor: null, - } - } - - return { - data: builds.map( - (build): ListedBuildDTO => ({ - id: build.id, - template: build.template, - templateId: build.templateId, - status: build.status, - statusMessage: build.statusMessage, - createdAt: new Date(build.createdAt).getTime(), - finishedAt: build.finishedAt - ? new Date(build.finishedAt).getTime() - : null, - }) - ), - nextCursor: result.data?.nextCursor ?? null, - } -} - -// get running build statuses - -async function getRunningStatuses( - accessToken: string, - teamId: string, - buildIds: string[] -): Promise { - if (buildIds.length === 0) { - return [] - } - - const result = await api.GET('/builds/statuses', { - params: { - query: { - build_ids: buildIds, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, - error: result.error, - teamId, - path: '/builds/statuses', - logKey: 'repositories:builds:get_running_statuses:dashboard_api_error', - }) - } - - return (result.data?.buildStatuses ?? []).map((row) => ({ - id: row.id, - status: row.status, - finishedAt: row.finishedAt ? new Date(row.finishedAt).getTime() : null, - statusMessage: row.statusMessage, - })) -} - -// get build details - -export async function getBuildInfo( - accessToken: string, - buildId: string, - teamId: string -): Promise { - const result = await api.GET('/builds/{build_id}', { - params: { - path: { - build_id: buildId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, - error: result.error, - teamId, - path: '/builds/{build_id}', - logKey: 'repositories:builds:get_build_info:dashboard_api_error', - context: { - build_id: buildId, - }, - }) - } - - const data = result.data - - return { - names: data.names ?? null, - createdAt: new Date(data.createdAt).getTime(), - finishedAt: data.finishedAt ? new Date(data.finishedAt).getTime() : null, - status: data.status, - statusMessage: data.statusMessage, - } -} - -// get build status (without logs) - -export async function getInfraBuildStatus( - accessToken: string, - teamId: string, - templateId: string, - buildId: string -) { - const result = await infra.GET( - `/templates/{templateID}/builds/{buildID}/status`, - { - params: { - path: { - templateID: templateId, - buildID: buildId, - }, - query: { - limit: 0, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - } - ) - - if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, - error: result.error, - teamId, - path: '/templates/{templateID}/builds/{buildID}/status', - logKey: 'repositories:builds:get_build_status:infra_error', - }) - } - - return result.data -} - -// get build logs - -export interface GetInfraBuildLogsOptions { - cursor?: number - limit?: number - direction?: 'forward' | 'backward' - level?: 'debug' | 'info' | 'warn' | 'error' -} - -export async function getInfraBuildLogs( - accessToken: string, - teamId: string, - templateId: string, - buildId: string, - options: GetInfraBuildLogsOptions = {} -) { - const result = await infra.GET( - `/templates/{templateID}/builds/{buildID}/logs`, - { - params: { - path: { - templateID: templateId, - buildID: buildId, - }, - query: { - cursor: options.cursor, - limit: options.limit, - direction: options.direction, - level: options.level, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - } - ) - - if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, - error: result.error, - teamId, - path: '/templates/{templateID}/builds/{buildID}/logs', - logKey: 'repositories:builds:get_build_logs:infra_error', - }) - } - - return result.data -} - -export const buildsRepo = { - listBuilds, - getRunningStatuses, - getBuildInfo, - getInfraBuildStatus, - getInfraBuildLogs, -} diff --git a/src/server/api/repositories/sandboxes.repository.ts b/src/server/api/repositories/sandboxes.repository.ts deleted file mode 100644 index 3508d2e10..000000000 --- a/src/server/api/repositories/sandboxes.repository.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { TRPCError } from '@trpc/server' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { api, infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { - apiError, - handleDashboardApiError, - handleInfraApiError, -} from '../errors' -import type { SandboxEventDTO } from '../models/sandboxes.models' - -// get sandbox logs - -export interface GetSandboxLogsOptions { - cursor?: number - limit?: number - direction?: 'forward' | 'backward' - level?: 'debug' | 'info' | 'warn' | 'error' - search?: string -} - -export async function getSandboxLogs( - accessToken: string, - teamId: string, - sandboxId: string, - options: GetSandboxLogsOptions = {} -) { - const result = await infra.GET('/v2/sandboxes/{sandboxID}/logs', { - params: { - path: { - sandboxID: sandboxId, - }, - query: { - cursor: options.cursor, - limit: options.limit, - direction: options.direction, - level: options.level, - search: options.search, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (!result.response.ok || result.error) { - const status = result.response.status - - l.error( - { - key: 'repositories:sandboxes:get_sandbox_logs:infra_error', - error: result.error, - team_id: teamId, - context: { - status, - path: '/v2/sandboxes/{sandboxID}/logs', - sandbox_id: sandboxId, - }, - }, - `failed to fetch /v2/sandboxes/{sandboxID}/logs: ${result.error?.message || 'Unknown error'}` - ) - - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) - } - - throw apiError(status) - } - - return result.data -} - -export async function getSandboxDetails( - accessToken: string, - teamId: string, - sandboxId: string -) { - const infraResult = await infra.GET('/sandboxes/{sandboxID}', { - params: { - path: { - sandboxID: sandboxId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - cache: 'no-store', - }) - - if (infraResult.response.ok && infraResult.data) { - return { - source: 'infra' as const, - details: infraResult.data, - } - } - - const infraStatus = infraResult.response.status - - if (infraStatus !== 404) { - handleInfraApiError({ - status: infraStatus, - error: infraResult.error, - teamId, - path: '/sandboxes/{sandboxID}', - logKey: 'repositories:sandboxes:get_sandbox_details:infra_error', - context: { - sandbox_id: sandboxId, - }, - }) - } - - const dashboardResult = await api.GET('/sandboxes/{sandboxID}/record', { - params: { - path: { - sandboxID: sandboxId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - cache: 'no-store', - }) - - if (dashboardResult.response.ok && dashboardResult.data) { - return { - source: 'database-record' as const, - details: dashboardResult.data, - } - } - - const dashboardStatus = dashboardResult.response.status - - if (dashboardStatus === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) - } - - handleDashboardApiError({ - status: dashboardStatus, - error: dashboardResult.error, - teamId, - path: '/sandboxes/{sandboxID}/record', - logKey: 'repositories:sandboxes:get_sandbox_details:fallback_error', - context: { - infra_status: infraStatus, - sandbox_id: sandboxId, - }, - }) -} - -const SANDBOX_EVENTS_PAGE_SIZE = 100 -const SANDBOX_EVENTS_MAX_PAGES = 50 -const SANDBOX_LIFECYCLE_EVENT_PREFIX = 'sandbox.lifecycle.' - -export async function getSandboxLifecycleEvents( - accessToken: string, - teamId: string, - sandboxId: string -) { - const lifecycleEvents: SandboxEventDTO[] = [] - - for ( - let pageIndex = 0, offset = 0; - pageIndex < SANDBOX_EVENTS_MAX_PAGES; - pageIndex += 1, offset += SANDBOX_EVENTS_PAGE_SIZE - ) { - try { - const result = await infra.GET('/events/sandboxes/{sandboxID}', { - params: { - path: { - sandboxID: sandboxId, - }, - query: { - offset, - limit: SANDBOX_EVENTS_PAGE_SIZE, - orderAsc: true, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - cache: 'no-store', - }) - - if (!result.response.ok || result.error) { - l.warn({ - key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_error', - error: result.error, - team_id: teamId, - context: { - status: result.response.status, - path: '/events/sandboxes/{sandboxID}', - sandbox_id: sandboxId, - offset, - limit: SANDBOX_EVENTS_PAGE_SIZE, - }, - }) - break - } - - const page = result.data ?? [] - lifecycleEvents.push( - ...page.filter((event) => - event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX) - ) - ) - - if (page.length < SANDBOX_EVENTS_PAGE_SIZE) { - break - } - } catch (error) { - l.warn({ - key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_exception', - error, - team_id: teamId, - context: { - path: '/events/sandboxes/{sandboxID}', - sandbox_id: sandboxId, - offset, - limit: SANDBOX_EVENTS_PAGE_SIZE, - }, - }) - break - } - } - - return lifecycleEvents -} - -// get sandbox metrics - -export interface GetSandboxMetricsOptions { - startUnixMs: number - endUnixMs: number -} - -export async function getSandboxMetrics( - accessToken: string, - teamId: string, - sandboxId: string, - options: GetSandboxMetricsOptions -) { - // convert milliseconds to seconds for the API - const startUnixSeconds = Math.floor(options.startUnixMs / 1000) - const endUnixSeconds = Math.floor(options.endUnixMs / 1000) - - const result = await infra.GET('/sandboxes/{sandboxID}/metrics', { - params: { - path: { - sandboxID: sandboxId, - }, - query: { - start: startUnixSeconds, - end: endUnixSeconds, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - - if (!result.response.ok || result.error) { - const status = result.response.status - - l.error( - { - key: 'repositories:sandboxes:get_sandbox_metrics:infra_error', - error: result.error, - team_id: teamId, - context: { - status, - path: '/sandboxes/{sandboxID}/metrics', - sandbox_id: sandboxId, - }, - }, - `failed to fetch /sandboxes/{sandboxID}/metrics: ${result.error?.message || 'Unknown error'}` - ) - - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) - } - - throw apiError(status) - } - - return result.data -} - -export const sandboxesRepo = { - getSandboxLogs, - getSandboxDetails, - getSandboxLifecycleEvents, - getSandboxMetrics, -} diff --git a/src/server/api/repositories/support.repository.ts b/src/server/api/repositories/support.repository.ts deleted file mode 100644 index 8dba3c9f8..000000000 --- a/src/server/api/repositories/support.repository.ts +++ /dev/null @@ -1,325 +0,0 @@ -import 'server-only' - -import { AttachmentType, PlainClient } from '@team-plain/typescript-sdk' -import { TRPCError } from '@trpc/server' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { api } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' - -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB per file -const MAX_FILES = 5 - -interface FileInput { - name: string - type: string - base64: string -} - -function formatThreadText(input: { - description: string - teamId: string - customerEmail: string - accountOwnerEmail: string - customerTier: string -}): string { - const { - description, - teamId, - customerEmail, - accountOwnerEmail, - customerTier, - } = input - - const header = [ - '########', - `Customer: ${customerEmail}`, - `Account Owner: ${accountOwnerEmail}`, - `Tier: ${customerTier}`, - `TeamID: ${teamId}`, - `Orbit: https://orbit.e2b.dev/teams/${teamId}/users`, - '########', - ].join('\n') - - const truncatedDescription = description.slice(0, 10000) - - return `${header}\n\n${truncatedDescription}` -} - -async function uploadAttachmentToPlain( - client: PlainClient, - customerId: string, - file: FileInput -): Promise { - const buffer = Buffer.from(file.base64, 'base64') - - if (buffer.byteLength > MAX_FILE_SIZE) { - throw new Error(`File ${file.name} exceeds 10MB limit`) - } - - l.info( - { - key: 'repositories:support:attachment_upload_start', - file_name: file.name, - file_size: buffer.byteLength, - file_type: file.type, - customer_id: customerId, - }, - `starting attachment upload for ${file.name} (${buffer.byteLength} bytes, type: ${file.type})` - ) - - const uploadUrlResult = await client.createAttachmentUploadUrl({ - customerId, - fileName: file.name, - fileSizeBytes: buffer.byteLength, - attachmentType: AttachmentType.CustomTimelineEntry, - }) - - if (uploadUrlResult.error) { - l.error( - { - key: 'repositories:support:attachment_create_url_error', - error: uploadUrlResult.error, - file_name: file.name, - }, - `failed to create upload URL for ${file.name}: ${uploadUrlResult.error.message}` - ) - throw new Error( - `Failed to create upload URL for ${file.name}: ${uploadUrlResult.error.message}` - ) - } - - const { uploadFormUrl, uploadFormData, attachment } = uploadUrlResult.data - - l.info( - { - key: 'repositories:support:attachment_uploading', - file_name: file.name, - attachment_id: attachment.id, - upload_url: uploadFormUrl, - form_data_keys: uploadFormData.map((d) => d.key), - }, - `uploading ${file.name} to Plain (attachment: ${attachment.id})` - ) - - const formData = new FormData() - for (const { key, value } of uploadFormData) { - formData.append(key, value) - } - formData.append('file', new Blob([buffer], { type: file.type }), file.name) - - const uploadResponse = await fetch(uploadFormUrl, { - method: 'POST', - body: formData, - }) - - if (!uploadResponse.ok) { - const responseText = await uploadResponse - .text() - .catch(() => 'unable to read response body') - l.error( - { - key: 'repositories:support:attachment_upload_failed', - file_name: file.name, - status: uploadResponse.status, - status_text: uploadResponse.statusText, - response_body: responseText, - }, - `failed to upload ${file.name}: ${uploadResponse.status} ${uploadResponse.statusText} - ${responseText}` - ) - throw new Error( - `Failed to upload ${file.name}: ${uploadResponse.status} ${uploadResponse.statusText}` - ) - } - - l.info( - { - key: 'repositories:support:attachment_upload_success', - file_name: file.name, - attachment_id: attachment.id, - }, - `successfully uploaded ${file.name} (attachment: ${attachment.id})` - ) - - return attachment.id -} - -export async function getTeamSupportData( - teamId: string, - accessToken: string -) { - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(accessToken), - }) - - if (error) { - l.error( - { - key: 'repositories:support:fetch_team_error', - error, - team_id: teamId, - }, - `failed to fetch team data: ${error.message}` - ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to load team information', - }) - } - - const team = data?.teams?.find((t) => t.id === teamId) - - if (!team) { - l.error( - { - key: 'repositories:support:fetch_team_not_found', - team_id: teamId, - }, - `team not found in user teams` - ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to load team information', - }) - } - - return { name: team.name, email: team.email, tier: team.tier } -} - -export async function createSupportThread(input: { - description: string - files?: FileInput[] - teamId: string - teamName: string - customerEmail: string - accountOwnerEmail: string - customerTier: string -}) { - const { - description, - files, - teamId, - teamName, - customerEmail, - accountOwnerEmail, - customerTier, - } = input - - if (!process.env.PLAIN_API_KEY) { - l.error( - { key: 'repositories:support:plain_not_configured' }, - 'PLAIN_API_KEY not configured' - ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Support API not configured', - }) - } - - const client = new PlainClient({ - apiKey: process.env.PLAIN_API_KEY, - }) - - // Upsert customer in Plain - const customerResult = await client.upsertCustomer({ - identifier: { - emailAddress: customerEmail, - }, - onCreate: { - email: { - email: customerEmail, - isVerified: true, - }, - fullName: customerEmail, - }, - onUpdate: {}, - }) - - if (customerResult.error) { - l.error( - { - key: 'repositories:support:upsert_customer_error', - error: customerResult.error, - customer_email: customerEmail, - }, - `failed to upsert customer in Plain: ${customerResult.error.message}` - ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to create support ticket', - }) - } - - const customerId = customerResult.data.customer.id - - // Upload attachments to Plain - const attachmentIds: string[] = [] - const validFiles = (files ?? []).slice(0, MAX_FILES) - - for (const file of validFiles) { - try { - const attachmentId = await uploadAttachmentToPlain( - client, - customerId, - file - ) - attachmentIds.push(attachmentId) - } catch (err) { - l.warn( - { - key: 'repositories:support:attachment_upload_error', - error: err, - file_name: file.name, - }, - `failed to upload attachment ${file.name}` - ) - // Continue with remaining files - } - } - - // Create thread - const title = `Support Request [${teamName}]` - const threadText = formatThreadText({ - description, - teamId, - customerEmail, - accountOwnerEmail, - customerTier, - }) - - const result = await client.createThread({ - title, - customerIdentifier: { - customerId, - }, - components: [ - { - componentText: { - text: threadText, - }, - }, - ], - ...(attachmentIds.length > 0 ? { attachmentIds } : {}), - }) - - if (result.error) { - l.error( - { - key: 'repositories:support:create_thread_error', - error: result.error, - customer_email: customerEmail, - }, - `failed to create Plain thread: ${result.error.message}` - ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to create support ticket', - }) - } - - return { threadId: result.data.id } -} - -export const supportRepo = { - getTeamSupportData, - createSupportThread, -} diff --git a/src/server/api/routers/billing.ts b/src/server/api/routers/billing.ts deleted file mode 100644 index c7772e658..000000000 --- a/src/server/api/routers/billing.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { TRPCError } from '@trpc/server' -import { headers } from 'next/headers' -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { - ADDON_500_SANDBOXES_ID, - ADDON_PURCHASE_ACTION_ERRORS, -} from '@/features/dashboard/billing/constants' -import { api } from '@/lib/clients/api' -import type { - AddOnOrderConfirmResponse, - AddOnOrderCreateResponse, - BillingLimit, - CustomerPortalResponse, - Invoice, - PaymentMethodsCustomerSession, - TeamItems, - UsageResponse, -} from '@/types/billing.types' -import { createTRPCRouter } from '../init' -import { protectedTeamProcedure } from '../procedures' - -function limitTypeToKey(type: 'limit' | 'alert') { - return type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte' -} - -export const billingRouter = createTRPCRouter({ - createCheckout: protectedTeamProcedure - .input(z.object({ tierId: z.string() })) - .mutation(async ({ ctx, input }) => { - const { teamId, session } = ctx - const { tierId } = input - - const res = await fetch(`${process.env.BILLING_API_URL}/checkouts`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - body: JSON.stringify({ - teamID: teamId, - tierID: tierId, - }), - }) - - if (!res.ok) { - const text = await res.text() - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to create checkout session', - }) - } - - const data = (await res.json()) as { url: string; error?: string } - - if (data.error) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: data.error, - }) - } - - return { url: data.url } - }), - - createCustomerPortalSession: protectedTeamProcedure.mutation( - async ({ ctx }) => { - const { teamId, session } = ctx - - const origin = (await headers()).get('origin') - - const res = await fetch(`${process.env.BILLING_API_URL}/stripe/portal`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(origin && { Origin: origin }), - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - }) - - if (!res.ok) { - const text = await res.text() - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to create customer portal session', - }) - } - - const data = (await res.json()) as CustomerPortalResponse - - return { url: data.url } - } - ), - - getItems: protectedTeamProcedure.query(async ({ ctx }) => { - const { teamId, session } = ctx - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/items`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: - text ?? `Failed to fetch billing endpoint: /teams/${teamId}/items`, - }) - } - - const items = (await res.json()) as TeamItems - - return items - }), - - getUsage: protectedTeamProcedure.query(async ({ ctx }) => { - const { teamId, session } = ctx - - const res = await fetch( - `${process.env.BILLING_API_URL}/v2/teams/${teamId}/usage`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to fetch usage data', - }) - } - - const responseData: UsageResponse = await res.json() - - // convert unix seconds to milliseconds because JavaScript - const data: UsageResponse = { - ...responseData, - hour_usages: responseData.hour_usages.map((hour) => ({ - ...hour, - timestamp: hour.timestamp * 1000, - })), - } - - return data - }), - - getInvoices: protectedTeamProcedure.query(async ({ ctx }) => { - const { teamId, session } = ctx - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/invoices`, - { - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: - text ?? `Failed to fetch billing endpoint: /teams/${teamId}/invoices`, - }) - } - - const invoices = (await res.json()) as Invoice[] - - return invoices - }), - - getLimits: protectedTeamProcedure.query(async ({ ctx }) => { - const { teamId, session } = ctx - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/billing-limits`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: - text ?? - `Failed to fetch billing endpoint: /teams/${teamId}/billing-limits`, - }) - } - - const limits = (await res.json()) as BillingLimit - - return limits - }), - - getTeamLimits: protectedTeamProcedure.query(async ({ ctx }) => { - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(ctx.session.access_token), - }) - - if (error || !data?.teams) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to fetch team limits', - }) - } - - const team = data.teams.find((t) => t.id === ctx.teamId) - - if (!team) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Team not found', - }) - } - - return { - concurrentInstances: team.limits.concurrentSandboxes, - diskMb: team.limits.diskMb, - maxLengthHours: team.limits.maxLengthHours, - maxRamMb: team.limits.maxRamMb, - maxVcpu: team.limits.maxVcpu, - } - }), - - setLimit: protectedTeamProcedure - .input( - z.object({ - type: z.enum(['limit', 'alert']), - value: z.number().min(1), - }) - ) - .mutation(async ({ ctx, input }) => { - const { teamId, session } = ctx - const { type, value } = input - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/billing-limits`, - { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token), - }, - body: JSON.stringify({ - [limitTypeToKey(type)]: value, - }), - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to set limit', - }) - } - }), - - clearLimit: protectedTeamProcedure - .input(z.object({ type: z.enum(['limit', 'alert']) })) - .mutation(async ({ ctx, input }) => { - const { teamId, session } = ctx - const { type } = input - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/billing-limits/${limitTypeToKey(type)}`, - { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to clear limit', - }) - } - }), - - createOrder: protectedTeamProcedure - .input(z.object({ itemId: z.literal(ADDON_500_SANDBOXES_ID) })) - .mutation(async ({ ctx, input }) => { - const { teamId, session } = ctx - const { itemId } = input - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/orders`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - body: JSON.stringify({ - items: [{ name: itemId, quantity: 1 }], - }), - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to create order', - }) - } - - const data = (await res.json()) as AddOnOrderCreateResponse - - return data - }), - - confirmOrder: protectedTeamProcedure - .input(z.object({ orderId: z.string().uuid() })) - .mutation(async ({ ctx, input }) => { - const { teamId, session } = ctx - const { orderId } = input - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/orders/${orderId}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - if ( - text.includes( - 'Missing payment method, please update your payment information' - ) - ) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: ADDON_PURCHASE_ACTION_ERRORS.missingPaymentMethod, - }) - } - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to confirm order', - }) - } - - const data = (await res.json()) as AddOnOrderConfirmResponse - - return data - }), - - getCustomerSession: protectedTeamProcedure.mutation(async ({ ctx }) => { - const { teamId, session } = ctx - - const res = await fetch( - `${process.env.BILLING_API_URL}/teams/${teamId}/payment-methods/customer-session`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - ) - - if (!res.ok) { - const text = await res.text() - - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: text ?? 'Failed to fetch customer session', - }) - } - - const data = (await res.json()) as PaymentMethodsCustomerSession - - return data - }), -}) diff --git a/src/server/api/routers/sandboxes.ts b/src/server/api/routers/sandboxes.ts deleted file mode 100644 index 8fcad8571..000000000 --- a/src/server/api/routers/sandboxes.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { USE_MOCK_DATA } from '@/configs/flags' -import { - calculateTeamMetricsStep, - MOCK_SANDBOXES_DATA, - MOCK_TEAM_METRICS_DATA, - MOCK_TEAM_METRICS_MAX_DATA, -} from '@/configs/mock-data' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { - fillTeamMetricsWithZeros, - transformMetricsToClientMetrics, -} from '@/server/sandboxes/utils' -import { apiError } from '../errors' -import { createTRPCRouter } from '../init' -import { protectedTeamProcedure } from '../procedures' -import { - GetTeamMetricsMaxSchema, - GetTeamMetricsSchema, -} from '../schemas/sandboxes' - -export const sandboxesRouter = createTRPCRouter({ - // QUERIES - getSandboxes: protectedTeamProcedure.query(async ({ ctx }) => { - const { session, teamId } = ctx - - if (USE_MOCK_DATA) { - await new Promise((resolve) => setTimeout(resolve, 200)) - - const sandboxes = MOCK_SANDBOXES_DATA() - - return { - sandboxes, - } - } - - const sandboxesResponse = await infra.GET('/sandboxes', { - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - cache: 'no-store', - }) - - if (!sandboxesResponse.response.ok || sandboxesResponse.error) { - const status = sandboxesResponse.response.status - - l.error( - { - key: 'trpc:sandboxes:get_team_sandboxes:infra_error', - error: sandboxesResponse.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - }, - }, - `failed to fetch /sandboxes: ${sandboxesResponse.error?.message || 'Unknown error'}` - ) - - throw apiError(status) - } - - return { - sandboxes: sandboxesResponse.data, - } - }), - - getSandboxesMetrics: protectedTeamProcedure - .input( - z.object({ - sandboxIds: z.array(z.string()), - }) - ) - .query(async ({ ctx, input }) => { - const { session, teamId } = ctx - const { sandboxIds } = input - - if (sandboxIds.length === 0 || USE_MOCK_DATA) { - return { - metrics: {}, - } - } - - const metricsResponse = await infra.GET('/sandboxes/metrics', { - params: { - query: { - sandbox_ids: sandboxIds, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - cache: 'no-store', - }) - - if (!metricsResponse.response.ok || metricsResponse.error) { - const status = metricsResponse.response.status - - l.error( - { - key: 'trpc:sandboxes:get_team_sandboxes_metrics:infra_error', - error: metricsResponse.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - sandboxIds, - path: '/sandboxes/metrics', - }, - }, - `failed to fetch /sandboxes/metrics: ${metricsResponse.error?.message || 'Unknown error'}` - ) - - throw apiError(status) - } - - const metrics = transformMetricsToClientMetrics( - metricsResponse.data.sandboxes - ) - - return { - metrics, - } - }), - - getTeamMetrics: protectedTeamProcedure - .input(GetTeamMetricsSchema) - .query(async ({ ctx, input }) => { - const { session, teamId } = ctx - const { startDate: startDateMs, endDate: endDateMs } = input - - // use mock data if enabled - if (USE_MOCK_DATA) { - const mockData = MOCK_TEAM_METRICS_DATA(startDateMs, endDateMs) - const filledMetrics = fillTeamMetricsWithZeros( - mockData.metrics, - startDateMs, - endDateMs, - mockData.step - ) - return { - metrics: filledMetrics, - step: mockData.step, - } - } - - const startS = Math.floor(startDateMs / 1000) - const endS = Math.floor(endDateMs / 1000) - - // calculate step to determine overfetch amount - const stepMs = calculateTeamMetricsStep(startDateMs, endDateMs) - - // overfetch by one step - // the overfetch is accounted for when post-processing the data using fillTeamMetricsWithZeros - const overfetchS = Math.ceil(stepMs / 1000) - - const res = await infra.GET('/teams/{teamID}/metrics', { - params: { - path: { - teamID: teamId, - }, - query: { - start: startS, - end: endS + overfetchS, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - cache: 'no-store', - }) - - if (!res.response.ok || res.error) { - const status = res.response.status - - l.warn( - { - key: `trpc:sandboxes:get_team_metrics:infra_error`, - error: res.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - startMs: startDateMs, - endMs: endDateMs, - stepMs, - overfetchS, - }, - }, - `failed to fetch /teams/{teamID}/metrics: ${res.error?.message || 'Unknown error'}` - ) - - throw apiError(status) - } - - // transform timestamps from seconds to milliseconds - const metrics = res.data.map((d) => ({ - concurrentSandboxes: d.concurrentSandboxes, - sandboxStartRate: d.sandboxStartRate, - timestamp: d.timestampUnix * 1000, - })) - - // fill gaps with zeros for smooth visualization - const filledMetrics = fillTeamMetricsWithZeros( - metrics, - startDateMs, - endDateMs, - stepMs - ) - - return { - metrics: filledMetrics, - step: stepMs, - } - }), - - getTeamMetricsMax: protectedTeamProcedure - .input(GetTeamMetricsMaxSchema) - .query(async ({ ctx, input }) => { - const { session, teamId } = ctx - const { startDate: startDateMs, endDate: endDateMs, metric } = input - - if (USE_MOCK_DATA) { - return MOCK_TEAM_METRICS_MAX_DATA(startDateMs, endDateMs, metric) - } - - // convert milliseconds to seconds for the API - const startS = Math.floor(startDateMs / 1000) - const endS = Math.floor(endDateMs / 1000) - - const res = await infra.GET('/teams/{teamID}/metrics/max', { - params: { - path: { - teamID: teamId, - }, - query: { - start: startS, - end: endS, - metric, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - cache: 'no-store', - }) - - if (!res.response.ok || res.error) { - const status = res.response.status - - l.error( - { - key: 'trpc:sandboxes:get_team_metrics_max:infra_error', - error: res.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - startDate: startDateMs, - endDate: endDateMs, - metric, - }, - }, - `failed to fetch /teams/{teamID}/metrics/max: ${res.error?.message || 'Unknown error'}` - ) - - throw apiError(status) - } - - // since javascript timestamps are in milliseconds, we want to convert the timestamp back to milliseconds - const timestampMs = res.data.timestampUnix * 1000 - - return { - timestamp: timestampMs, - value: res.data.value, - metric, - } - }), - - // MUTATIONS -}) diff --git a/src/server/api/routers/teams.ts b/src/server/api/routers/teams.ts deleted file mode 100644 index 9dfc7e7b3..000000000 --- a/src/server/api/routers/teams.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { TRPCError } from '@trpc/server' -import z from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { api } from '@/lib/clients/api' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import type { ClientTeam } from '@/types/dashboard.types' -import { protectedProcedure } from '../procedures' - -function mapApiTeamToClientTeam( - apiTeam: { - id: string - name: string - slug: string - tier: string - email: string - isDefault: boolean - }, -): ClientTeam { - return { - id: apiTeam.id, - name: apiTeam.name, - slug: apiTeam.slug, - tier: apiTeam.tier, - email: apiTeam.email, - is_default: apiTeam.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: null, - } -} - -export const teamsRouter = { - getCurrentTeam: protectedProcedure - .input(z.object({ teamIdOrSlug: TeamIdOrSlugSchema })) - .query(async ({ ctx, input }) => { - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(ctx.session.access_token), - }) - - if (error || !data?.teams) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to fetch teams', - }) - } - - const apiTeam = data.teams.find( - (t) => t.slug === input.teamIdOrSlug || t.id === input.teamIdOrSlug - ) - - if (!apiTeam) { - throw new TRPCError({ - code: 'FORBIDDEN', - message: 'Team not found or access denied', - }) - } - - return mapApiTeamToClientTeam(apiTeam) - }), -} diff --git a/src/server/api/routers/templates.ts b/src/server/api/routers/templates.ts deleted file mode 100644 index 79d393e34..000000000 --- a/src/server/api/routers/templates.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { TRPCError } from '@trpc/server' -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { CACHE_TAGS } from '@/configs/cache' -import { USE_MOCK_DATA } from '@/configs/flags' -import { - MOCK_DEFAULT_TEMPLATES_DATA, - MOCK_TEMPLATES_DATA, -} from '@/configs/mock-data' -import { api, infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import type { DefaultTemplate } from '@/types/api.types' -import { apiError } from '../errors' -import { createTRPCRouter } from '../init' -import { protectedProcedure, protectedTeamProcedure } from '../procedures' - -export const templatesRouter = createTRPCRouter({ - // QUERIES - - getTemplates: protectedTeamProcedure.query(async ({ ctx }) => { - const { session, teamId } = ctx - - if (USE_MOCK_DATA) { - await new Promise((resolve) => setTimeout(resolve, 500)) - return { - templates: MOCK_TEMPLATES_DATA, - } - } - - const res = await infra.GET('/templates', { - params: { - query: { - teamID: teamId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - }) - - if (!res.response.ok || res.error) { - const status = res.response.status - - l.error( - { - key: 'trpc:templates:get_team_templates:infra_error', - error: res.error, - team_id: teamId, - user_id: session.user.id, - context: { - status, - }, - }, - `failed to fetch /templates: ${res.error?.message || 'Unknown error'}` - ) - - throw apiError(status) - } - - return { - templates: res.data, - } - }), - - getDefaultTemplatesCached: protectedProcedure.query(async ({ ctx }) => { - return getDefaultTemplatesCached(ctx.session.access_token) - }), - - // MUTATIONS - - deleteTemplate: protectedTeamProcedure - .input( - z.object({ - templateId: z.string(), - }) - ) - .mutation(async ({ ctx, input }) => { - const { session, teamId } = ctx - const { templateId } = input - - const res = await infra.DELETE('/templates/{templateID}', { - params: { - path: { - templateID: templateId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - }) - - if (!res.response.ok || res.error) { - const status = res.response.status - - l.error( - { - key: 'trpc:templates:delete_template:infra_error', - error: res.error, - user_id: session.user.id, - team_id: teamId, - template_id: templateId, - context: { - status, - }, - }, - `failed to delete /templates/{templateID}: ${res.error?.message || 'Unknown error'}` - ) - - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Template not found', - }) - } - - if ( - status === 400 && - res.error?.message?.includes( - 'because there are paused sandboxes using it' - ) - ) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: - 'Cannot delete template because there are paused sandboxes using it', - }) - } - - throw apiError(status) - } - - return { success: true } - }), - - updateTemplate: protectedTeamProcedure - .input( - z.object({ - templateId: z.string(), - public: z.boolean(), - }) - ) - .mutation(async ({ ctx, input }) => { - const { session, teamId } = ctx - const { templateId, public: isPublic } = input - - const res = await infra.PATCH('/templates/{templateID}', { - body: { - public: isPublic, - }, - params: { - path: { - templateID: templateId, - }, - }, - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - }) - - if (!res.response.ok || res.error) { - const status = res.response.status - - l.error( - { - key: 'trpc:templates:update_template:infra_error', - error: res.error, - user_id: session.user.id, - team_id: teamId, - template_id: templateId, - context: { - status, - }, - }, - `failed to patch /templates/{templateID}: ${res.error?.message || 'Unknown error'}` - ) - - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Template not found', - }) - } - - throw apiError(status) - } - - return { success: true, public: isPublic } - }), -}) - -async function getDefaultTemplatesCached(accessToken: string) { - if (USE_MOCK_DATA) { - await new Promise((resolve) => setTimeout(resolve, 500)) - return { - templates: MOCK_DEFAULT_TEMPLATES_DATA, - } - } - - const { data, error } = await api.GET('/templates/defaults', { - headers: SUPABASE_AUTH_HEADERS(accessToken), - next: { tags: [CACHE_TAGS.DEFAULT_TEMPLATES] }, - }) - - if (error) { - throw new Error(error.message) - } - - if (!data?.templates || data.templates.length === 0) { - return { templates: [] as DefaultTemplate[] } - } - - const templates: DefaultTemplate[] = data.templates.map((t) => ({ - templateID: t.id, - buildID: t.buildId, - cpuCount: t.vcpu, - memoryMB: t.ramMb, - diskSizeMB: t.totalDiskSizeMb ?? 0, - envdVersion: t.envdVersion ?? '', - public: t.public, - aliases: t.aliases.map((a) => a.alias), - names: t.aliases.map((a) => { - if (a.namespace && a.namespace.length > 0) { - return `${a.namespace}/${a.alias}` - } - return a.alias - }), - createdAt: t.createdAt, - updatedAt: t.createdAt, - createdBy: null, - lastSpawnedAt: t.createdAt, - spawnCount: t.spawnCount, - buildCount: t.buildCount, - isDefault: true as const, - })) - - return { templates } -} diff --git a/src/server/team/get-team-members.ts b/src/server/team/get-team-members.ts deleted file mode 100644 index 98a4fa077..000000000 --- a/src/server/team/get-team-members.ts +++ /dev/null @@ -1,59 +0,0 @@ -import 'server-only' - -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { api } from '@/lib/clients/api' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import type { TeamMemberInfo } from './types' - -const GetTeamMembersSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, -}) - -export const getTeamMembers = authActionClient - .schema(GetTeamMembersSchema) - .metadata({ serverFunctionName: 'getTeamMembers' }) - .use(withTeamIdResolution) - .action(async ({ ctx }) => { - const { teamId, session } = ctx - - const { data, error } = await api.GET('/teams/{teamId}/members', { - params: { path: { teamId } }, - headers: SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }) - - if (error) { - throw new Error(error.message) - } - - if (!data?.members || data.members.length === 0) { - return [] - } - - const enrichedMembers = await Promise.all( - data.members.map(async (member) => { - const { data: userData } = - await supabaseAdmin.auth.admin.getUserById(member.id) - - const user = userData.user - const info: TeamMemberInfo = { - id: member.id, - email: member.email, - name: user?.user_metadata?.name, - avatar_url: user?.user_metadata?.avatar_url, - } - - return { - info, - relation: { - added_by: member.addedBy ?? null, - is_default: member.isDefault, - }, - } - }) - ) - - return enrichedMembers - }) diff --git a/src/server/team/get-team.ts b/src/server/team/get-team.ts deleted file mode 100644 index 695d81a8f..000000000 --- a/src/server/team/get-team.ts +++ /dev/null @@ -1,83 +0,0 @@ -import 'server-cli-only' - -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { api } from '@/lib/clients/api' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' -import type { ClientTeam } from '@/types/dashboard.types' - -const GetTeamSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, -}) - -export const getTeam = authActionClient - .schema(GetTeamSchema) - .metadata({ serverFunctionName: 'getTeam' }) - .use(withTeamIdResolution) - .action(async ({ ctx }) => { - const { teamId, session } = ctx - - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(session.access_token), - }) - - if (error || !data?.teams) { - return returnServerError('Failed to fetch team') - } - - const apiTeam = data.teams.find((t) => t.id === teamId) - - if (!apiTeam) { - return returnServerError('Team not found') - } - - const team: ClientTeam = { - id: apiTeam.id, - name: apiTeam.name, - slug: apiTeam.slug, - tier: apiTeam.tier, - email: apiTeam.email, - is_default: apiTeam.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: null, - } - - return team - }) - -export const getUserTeams = authActionClient - .metadata({ serverFunctionName: 'getUserTeams' }) - .action(async ({ ctx }) => { - const { session } = ctx - - const { data, error } = await api.GET('/teams', { - headers: SUPABASE_AUTH_HEADERS(session.access_token), - }) - - if (error || !data?.teams || data.teams.length === 0) { - return returnServerError('No teams found.') - } - - const teams: ClientTeam[] = data.teams.map((t) => ({ - id: t.id, - name: t.name, - slug: t.slug, - tier: t.tier, - email: t.email, - is_default: t.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: null, - })) - - return teams - }) diff --git a/src/server/team/types.ts b/src/server/team/types.ts deleted file mode 100644 index 61441a2cf..000000000 --- a/src/server/team/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { z } from 'zod' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' - -export type TeamMemberInfo = { - id: string - email: string - name: string - avatar_url: string -} - -export type TeamMemberRelation = { - added_by: string | null - is_default: boolean -} - -export type TeamMember = { - info: TeamMemberInfo - relation: TeamMemberRelation -} - -/** - * Valid: "Team 1", "DevOps2023", "Engineering Team", "Dev-Ops", "Team_Name", "Team.Name" - * Invalid: empty strings, "Team@Work", "Team--Name", names > 32 chars - */ -export const TeamNameSchema = z - .string() - .trim() - .min(1, { message: 'Team name cannot be empty' }) - .max(32, { message: 'Team name cannot be longer than 32 characters' }) - .regex(/^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$/, { - message: - 'Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots', - }) - -// Shared schemas - -const UpdateTeamNameSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, - name: TeamNameSchema, -}) - -const CreateTeamSchema = z.object({ - name: TeamNameSchema, -}) - -export { CreateTeamSchema, UpdateTeamNameSchema } - -/** - * The result of resolving a team for a user. - * Contains the team ID, slug. - */ -export interface ResolvedTeam { - id: string - slug: string -} diff --git a/src/server/usage/get-usage.ts b/src/server/usage/get-usage.ts deleted file mode 100644 index ac4449afa..000000000 --- a/src/server/usage/get-usage.ts +++ /dev/null @@ -1,58 +0,0 @@ -import 'server-only' - -import { cacheLife, cacheTag } from 'next/cache' -import { z } from 'zod' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' -import type { UsageResponse } from '@/types/billing.types' - -const GetUsageAuthActionSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, -}) - -export const getUsage = authActionClient - .schema(GetUsageAuthActionSchema) - .metadata({ serverFunctionName: 'getUsage' }) - .use(withTeamIdResolution) - .action(async ({ ctx }) => { - 'use cache' - - const { teamId } = ctx - - cacheLife('hours') - cacheTag(CACHE_TAGS.TEAM_USAGE(teamId)) - - const accessToken = ctx.session.access_token - - const response = await fetch( - `${process.env.BILLING_API_URL}/v2/teams/${teamId}/usage`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - } - ) - - if (!response.ok) { - const text = (await response.text()) ?? 'Failed to fetch usage data' - return returnServerError(text) - } - - const responseData: UsageResponse = await response.json() - - // convert unix seconds to milliseconds because JavaScript - const data: UsageResponse = { - ...responseData, - hour_usages: responseData.hour_usages.map((hour) => ({ - ...hour, - timestamp: hour.timestamp * 1000, - })), - } - - return data - }) diff --git a/src/trpc/client.tsx b/src/trpc/client.tsx index 362435c67..9accffd8d 100644 --- a/src/trpc/client.tsx +++ b/src/trpc/client.tsx @@ -8,7 +8,7 @@ import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server' import { createTRPCContext } from '@trpc/tanstack-react-query' import { useState } from 'react' import SuperJSON from 'superjson' -import type { TRPCAppRouter } from '@/server/api/routers' +import type { TRPCAppRouter } from '@/core/server/api/routers' import { createQueryClient } from './query-client' export const { TRPCProvider, useTRPC, useTRPCClient } = diff --git a/src/trpc/server.tsx b/src/trpc/server.tsx index 941427cce..54ab40f24 100644 --- a/src/trpc/server.tsx +++ b/src/trpc/server.tsx @@ -7,9 +7,8 @@ import { } from '@trpc/tanstack-react-query' import { headers } from 'next/headers' import { cache } from 'react' - -import { createTRPCContext } from '@/server/api/init' -import { createTRPCCaller, trpcAppRouter } from '@/server/api/routers' +import { createTRPCCaller, trpcAppRouter } from '@/core/server/api/routers' +import { createTRPCContext } from '@/core/server/trpc/init' import { createQueryClient } from './query-client' /** diff --git a/src/types/dashboard-api.types.ts b/src/types/dashboard-api.types.ts index f1528f078..be5848fcc 100644 --- a/src/types/dashboard-api.types.ts +++ b/src/types/dashboard-api.types.ts @@ -4,792 +4,792 @@ */ export interface paths { - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Health check */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Health check successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List team builds */ - get: { - parameters: { - query?: { - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template?: components["parameters"]["build_id_or_template"]; - /** @description Comma-separated list of build statuses to include. */ - statuses?: components["parameters"]["build_statuses"]; - /** @description Maximum number of items to return per page. */ - limit?: components["parameters"]["builds_limit"]; - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - cursor?: components["parameters"]["builds_cursor"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned paginated builds. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildsListResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds/statuses": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get build statuses */ - get: { - parameters: { - query: { - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: components["parameters"]["build_ids"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned build statuses */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildsStatusesResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds/{build_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get build details */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the build. */ - build_id: components["parameters"]["build_id"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned build details. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildInfo"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/record": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get sandbox record */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the sandbox. */ - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned sandbox details. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxRecord"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List user teams - * @description Returns all teams the authenticated user belongs to, with limits and default flag. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned user teams. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserTeamsResponse"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/resolve": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Resolve team identity - * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. - */ - get: { - parameters: { - query: { - /** @description Team slug to resolve. */ - slug: components["parameters"]["teamSlug"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully resolved team. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamResolveResponse"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update team */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTeamRequest"]; - }; - }; - responses: { - /** @description Successfully updated team. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UpdateTeamResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - trace?: never; - }; - "/teams/{teamId}/members": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List team members */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned team members. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamMembersResponse"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** Add team member */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AddTeamMemberRequest"]; - }; - }; - responses: { - /** @description Successfully added team member. */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/members/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove team member */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - /** @description Identifier of the user. */ - userId: components["parameters"]["userId"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully removed team member. */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/defaults": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List default templates - * @description Returns the list of default templates with their latest build info and aliases. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned default templates. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DefaultTemplatesResponse"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; + '/health': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Health check */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Health check successful */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['HealthResponse'] + } + } + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List team builds */ + get: { + parameters: { + query?: { + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template?: components['parameters']['build_id_or_template'] + /** @description Comma-separated list of build statuses to include. */ + statuses?: components['parameters']['build_statuses'] + /** @description Maximum number of items to return per page. */ + limit?: components['parameters']['builds_limit'] + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + cursor?: components['parameters']['builds_cursor'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned paginated builds. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildsListResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds/statuses': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get build statuses */ + get: { + parameters: { + query: { + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: components['parameters']['build_ids'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned build statuses */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildsStatusesResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds/{build_id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get build details */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the build. */ + build_id: components['parameters']['build_id'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned build details. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildInfo'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/record': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get sandbox record */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the sandbox. */ + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned sandbox details. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxRecord'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * List user teams + * @description Returns all teams the authenticated user belongs to, with limits and default flag. + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned user teams. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UserTeamsResponse'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/resolve': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Resolve team identity + * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. + */ + get: { + parameters: { + query: { + /** @description Team slug to resolve. */ + slug: components['parameters']['teamSlug'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully resolved team. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamResolveResponse'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** Update team */ + patch: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateTeamRequest'] + } + } + responses: { + /** @description Successfully updated team. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateTeamResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + trace?: never + } + '/teams/{teamId}/members': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List team members */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned team members. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamMembersResponse'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + /** Add team member */ + post: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AddTeamMemberRequest'] + } + } + responses: { + /** @description Successfully added team member. */ + 201: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamId}/members/{userId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** Remove team member */ + delete: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + /** @description Identifier of the user. */ + userId: components['parameters']['userId'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully removed team member. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/defaults': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * List default templates + * @description Returns the list of default templates with their latest build info and aliases. + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned default templates. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['DefaultTemplatesResponse'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } } -export type webhooks = Record; +export type webhooks = Record export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code. - */ - code: number; - /** @description Error message. */ - message: string; - }; - /** - * @description Build status mapped for dashboard clients. - * @enum {string} - */ - BuildStatus: "building" | "failed" | "success"; - ListedBuild: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string; - /** @description Template alias when present, otherwise template ID. */ - template: string; - /** @description Identifier of the template. */ - templateId: string; - status: components["schemas"]["BuildStatus"]; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - }; - BuildsListResponse: { - data: components["schemas"]["ListedBuild"][]; - /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ - nextCursor: string | null; - }; - BuildStatusItem: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string; - status: components["schemas"]["BuildStatus"]; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - }; - BuildsStatusesResponse: { - /** @description List of build statuses */ - buildStatuses: components["schemas"]["BuildStatusItem"][]; - }; - BuildInfo: { - /** @description Template names related to this build, if available. */ - names?: string[] | null; - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - status: components["schemas"]["BuildStatus"]; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - }; - /** - * Format: int64 - * @description CPU cores for the sandbox - */ - CPUCount: number; - /** - * Format: int64 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number; - /** - * Format: int64 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number; - SandboxRecord: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string; - /** @description Alias of the template */ - alias?: string; - /** @description Identifier of the sandbox */ - sandboxID: string; - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string; - /** - * Format: date-time - * @description Time when the sandbox was stopped - */ - stoppedAt?: string | null; - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - }; - HealthResponse: { - /** @description Human-readable health check result. */ - message: string; - }; - UserTeamLimits: { - /** Format: int64 */ - maxLengthHours: number; - /** Format: int32 */ - concurrentSandboxes: number; - /** Format: int32 */ - concurrentTemplateBuilds: number; - /** Format: int32 */ - maxVcpu: number; - /** Format: int32 */ - maxRamMb: number; - /** Format: int32 */ - diskMb: number; - }; - UserTeam: { - /** Format: uuid */ - id: string; - name: string; - slug: string; - tier: string; - email: string; - isDefault: boolean; - limits: components["schemas"]["UserTeamLimits"]; - }; - UserTeamsResponse: { - teams: components["schemas"]["UserTeam"][]; - }; - TeamMember: { - /** Format: uuid */ - id: string; - email: string; - isDefault: boolean; - /** Format: uuid */ - addedBy?: string | null; - /** Format: date-time */ - createdAt: string | null; - }; - TeamMembersResponse: { - members: components["schemas"]["TeamMember"][]; - }; - UpdateTeamRequest: { - name: string; - }; - UpdateTeamResponse: { - /** Format: uuid */ - id: string; - name: string; - }; - AddTeamMemberRequest: { - /** Format: email */ - email: string; - }; - DefaultTemplateAlias: { - alias: string; - namespace?: string | null; - }; - DefaultTemplate: { - id: string; - aliases: components["schemas"]["DefaultTemplateAlias"][]; - /** Format: uuid */ - buildId: string; - /** Format: int64 */ - ramMb: number; - /** Format: int64 */ - vcpu: number; - /** Format: int64 */ - totalDiskSizeMb: number | null; - envdVersion?: string | null; - /** Format: date-time */ - createdAt: string; - public: boolean; - /** Format: int32 */ - buildCount: number; - /** Format: int64 */ - spawnCount: number; - }; - DefaultTemplatesResponse: { - templates: components["schemas"]["DefaultTemplate"][]; - }; - TeamResolveResponse: { - /** Format: uuid */ - id: string; - slug: string; - }; - }; - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - }; - parameters: { - /** @description Identifier of the build. */ - build_id: string; - /** @description Identifier of the sandbox. */ - sandboxID: string; - /** @description Maximum number of items to return per page. */ - builds_limit: number; - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - builds_cursor: string; - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template: string; - /** @description Comma-separated list of build statuses to include. */ - build_statuses: components["schemas"]["BuildStatus"][]; - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: string[]; - /** @description Identifier of the team. */ - teamId: string; - /** @description Identifier of the user. */ - userId: string; - /** @description Team slug to resolve. */ - teamSlug: string; - }; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + Error: { + /** + * Format: int32 + * @description Error code. + */ + code: number + /** @description Error message. */ + message: string + } + /** + * @description Build status mapped for dashboard clients. + * @enum {string} + */ + BuildStatus: 'building' | 'failed' | 'success' + ListedBuild: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string + /** @description Template alias when present, otherwise template ID. */ + template: string + /** @description Identifier of the template. */ + templateId: string + status: components['schemas']['BuildStatus'] + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + } + BuildsListResponse: { + data: components['schemas']['ListedBuild'][] + /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ + nextCursor: string | null + } + BuildStatusItem: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string + status: components['schemas']['BuildStatus'] + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + } + BuildsStatusesResponse: { + /** @description List of build statuses */ + buildStatuses: components['schemas']['BuildStatusItem'][] + } + BuildInfo: { + /** @description Template names related to this build, if available. */ + names?: string[] | null + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + status: components['schemas']['BuildStatus'] + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + } + /** + * Format: int64 + * @description CPU cores for the sandbox + */ + CPUCount: number + /** + * Format: int64 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number + /** + * Format: int64 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number + SandboxRecord: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string + /** @description Alias of the template */ + alias?: string + /** @description Identifier of the sandbox */ + sandboxID: string + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string + /** + * Format: date-time + * @description Time when the sandbox was stopped + */ + stoppedAt?: string | null + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + } + HealthResponse: { + /** @description Human-readable health check result. */ + message: string + } + UserTeamLimits: { + /** Format: int64 */ + maxLengthHours: number + /** Format: int32 */ + concurrentSandboxes: number + /** Format: int32 */ + concurrentTemplateBuilds: number + /** Format: int32 */ + maxVcpu: number + /** Format: int32 */ + maxRamMb: number + /** Format: int32 */ + diskMb: number + } + UserTeam: { + /** Format: uuid */ + id: string + name: string + slug: string + tier: string + email: string + isDefault: boolean + limits: components['schemas']['UserTeamLimits'] + } + UserTeamsResponse: { + teams: components['schemas']['UserTeam'][] + } + TeamMember: { + /** Format: uuid */ + id: string + email: string + isDefault: boolean + /** Format: uuid */ + addedBy?: string | null + /** Format: date-time */ + createdAt: string | null + } + TeamMembersResponse: { + members: components['schemas']['TeamMember'][] + } + UpdateTeamRequest: { + name: string + } + UpdateTeamResponse: { + /** Format: uuid */ + id: string + name: string + } + AddTeamMemberRequest: { + /** Format: email */ + email: string + } + DefaultTemplateAlias: { + alias: string + namespace?: string | null + } + DefaultTemplate: { + id: string + aliases: components['schemas']['DefaultTemplateAlias'][] + /** Format: uuid */ + buildId: string + /** Format: int64 */ + ramMb: number + /** Format: int64 */ + vcpu: number + /** Format: int64 */ + totalDiskSizeMb: number | null + envdVersion?: string | null + /** Format: date-time */ + createdAt: string + public: boolean + /** Format: int32 */ + buildCount: number + /** Format: int64 */ + spawnCount: number + } + DefaultTemplatesResponse: { + templates: components['schemas']['DefaultTemplate'][] + } + TeamResolveResponse: { + /** Format: uuid */ + id: string + slug: string + } + } + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + } + parameters: { + /** @description Identifier of the build. */ + build_id: string + /** @description Identifier of the sandbox. */ + sandboxID: string + /** @description Maximum number of items to return per page. */ + builds_limit: number + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + builds_cursor: string + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template: string + /** @description Comma-separated list of build statuses to include. */ + build_statuses: components['schemas']['BuildStatus'][] + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: string[] + /** @description Identifier of the team. */ + teamId: string + /** @description Identifier of the user. */ + userId: string + /** @description Team slug to resolve. */ + teamSlug: string + } + requestBodies: never + headers: never + pathItems: never } -export type $defs = Record; -export type operations = Record; +export type $defs = Record +export type operations = Record diff --git a/tsconfig.json b/tsconfig.json index 8175970da..80b35f972 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,33 @@ } ], "paths": { + "@/domains/*": ["./src/core/domains/*"], + "@/shared/*": ["./src/core/shared/*"], + "@/server/api/errors": ["./src/core/server/adapters/trpc-errors.ts"], + "@/server/api/models/builds.models": [ + "./src/core/domains/builds/models.ts" + ], + "@/server/api/models/sandboxes.models": [ + "./src/core/domains/sandboxes/models.ts" + ], + "@/server/api/models/auth.models": ["./src/core/domains/auth/models.ts"], + "@/server/api/schemas/sandboxes": [ + "./src/core/domains/sandboxes/schemas.ts" + ], + "@/server/api/init": ["./src/core/server/trpc/init.ts"], + "@/server/api/procedures": ["./src/core/server/trpc/procedures.ts"], + "@/server/api/routers": ["./src/core/server/api/routers/index.ts"], + "@/server/api/routers/*": ["./src/core/server/api/routers/*"], + "@/server/api/middlewares/*": ["./src/core/server/api/middlewares/*"], + "@/server/*": [ + "./src/core/server/actions/*", + "./src/core/server/context/*", + "./src/core/server/adapters/*", + "./src/core/server/trpc/*", + "./src/core/server/http/*", + "./src/core/server/functions/*", + "./src/core/server/api/*" + ], "@/*": ["./src/*"] }, "isolatedModules": true From baaddaca3d63c0e4a0b8392fd957f0c1dcda629e Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Tue, 17 Mar 2026 23:32:58 -0700 Subject: [PATCH 04/37] refactor: update file structure and type imports - Moved generated API types to a new directory structure under `src/core/shared/contracts`. - Updated paths in `tsconfig.json` to reflect the new locations of type definitions. - Refactored various imports across the codebase to use the new type paths. - Removed obsolete client and API files to streamline the codebase. --- package.json | 8 +- src/__test__/unit/chart-utils.test.ts | 2 +- .../unit/fill-metrics-with-zeros.test.ts | 2 +- src/app/api/teams/[teamId]/metrics/types.ts | 2 +- .../teams/[teamId]/sandboxes/metrics/types.ts | 2 +- src/app/api/teams/user/types.ts | 2 +- src/configs/mock-data.ts | 2 +- .../domains/billing/models.ts} | 54 +- src/core/domains/billing/repository.server.ts | 2 +- .../domains/sandboxes/models.client.ts} | 4 +- src/core/domains/sandboxes/models.ts | 6 + src/core/domains/teams/models.ts | 7 +- src/core/server/actions/auth-actions.ts | 2 +- .../server/actions/client.ts} | 40 +- src/core/server/actions/key-actions.ts | 2 +- src/core/server/actions/sandbox-actions.ts | 2 +- src/core/server/actions/team-actions.ts | 4 +- src/core/server/actions/user-actions.ts | 2 +- src/core/server/actions/utils.ts | 36 + src/core/server/actions/webhooks-actions.ts | 2 +- .../server/functions/keys/get-api-keys.ts | 2 +- .../sandboxes/get-team-metrics-core.ts | 2 +- .../sandboxes/get-team-metrics-max.ts | 2 +- .../functions/sandboxes/get-team-metrics.ts | 2 +- src/core/server/functions/sandboxes/utils.ts | 4 +- .../server/functions/team/get-team-limits.ts | 2 +- .../server/functions/team/get-team-members.ts | 2 +- src/core/server/functions/team/get-team.ts | 2 +- src/core/server/functions/usage/get-usage.ts | 2 +- .../server/functions/webhooks/get-webhooks.ts | 2 +- src/{lib => core/shared}/clients/api.ts | 6 +- src/{lib => core/shared}/clients/kv.ts | 0 .../shared}/clients/logger/logger.node.ts | 0 .../shared}/clients/logger/logger.ts | 28 - src/{lib => core/shared}/clients/meter.ts | 0 src/{lib => core/shared}/clients/storage.ts | 25 +- .../shared}/clients/supabase/admin.ts | 2 +- .../shared}/clients/supabase/client.ts | 2 +- .../shared}/clients/supabase/server.ts | 8 +- src/{lib => core/shared}/clients/tracer.ts | 0 src/core/shared/contracts/argus-api.types.ts | 479 +++ .../shared/contracts/dashboard-api.types.ts | 795 ++++ .../shared/contracts}/database.types.ts | 0 src/core/shared/contracts/infra-api.types.ts | 3219 ++++++++++++++++ src/core/shared/errors.ts | 40 + src/core/shared/schemas/api.ts | 7 + src/core/shared/schemas/team.ts | 3 + src/{lib => core/shared}/schemas/url.ts | 0 src/features/dashboard/billing/addons.tsx | 2 +- .../dashboard/billing/select-plan.tsx | 2 +- src/features/dashboard/billing/types.ts | 2 +- src/features/dashboard/billing/utils.ts | 2 +- src/features/dashboard/context.tsx | 2 +- src/features/dashboard/limits/alert-card.tsx | 2 +- src/features/dashboard/limits/limit-card.tsx | 2 +- .../sandboxes/list/stores/metrics-store.ts | 2 +- .../charts/team-metrics-chart/types.ts | 2 +- .../charts/team-metrics-chart/utils.ts | 2 +- src/features/dashboard/sidebar/menu-teams.tsx | 2 +- .../dashboard/usage/sampling-utils.ts | 2 +- .../dashboard/usage/usage-charts-context.tsx | 2 +- src/lib/hooks/use-team.ts | 2 +- src/lib/hooks/use-user.ts | 2 +- src/lib/schemas/api.ts | 13 - src/lib/schemas/team.ts | 11 - src/lib/utils/action.ts | 65 - src/lib/utils/rewrites.ts | 2 +- src/lib/utils/server.ts | 6 +- src/proxy.ts | 2 +- src/types/argus-api.types.ts | 479 --- src/types/dashboard-api.types.ts | 795 ---- src/types/dashboard.types.ts | 8 - src/types/errors.ts | 43 - src/types/infra-api.types.ts | 3222 ----------------- tsconfig.json | 11 + 75 files changed, 4684 insertions(+), 4822 deletions(-) rename src/{types/billing.types.ts => core/domains/billing/models.ts} (56%) rename src/{types/sandboxes.types.ts => core/domains/sandboxes/models.client.ts} (80%) rename src/{lib/clients/action.ts => core/server/actions/client.ts} (80%) create mode 100644 src/core/server/actions/utils.ts rename src/{lib => core/shared}/clients/api.ts (80%) rename src/{lib => core/shared}/clients/kv.ts (100%) rename src/{lib => core/shared}/clients/logger/logger.node.ts (100%) rename src/{lib => core/shared}/clients/logger/logger.ts (64%) rename src/{lib => core/shared}/clients/meter.ts (100%) rename src/{lib => core/shared}/clients/storage.ts (67%) rename src/{lib => core/shared}/clients/supabase/admin.ts (81%) rename src/{lib => core/shared}/clients/supabase/client.ts (74%) rename src/{lib => core/shared}/clients/supabase/server.ts (69%) rename src/{lib => core/shared}/clients/tracer.ts (100%) create mode 100644 src/core/shared/contracts/argus-api.types.ts create mode 100644 src/core/shared/contracts/dashboard-api.types.ts rename src/{types => core/shared/contracts}/database.types.ts (100%) create mode 100644 src/core/shared/contracts/infra-api.types.ts create mode 100644 src/core/shared/schemas/api.ts create mode 100644 src/core/shared/schemas/team.ts rename src/{lib => core/shared}/schemas/url.ts (100%) delete mode 100644 src/lib/schemas/api.ts delete mode 100644 src/lib/schemas/team.ts delete mode 100644 src/lib/utils/action.ts delete mode 100644 src/types/argus-api.types.ts delete mode 100644 src/types/dashboard-api.types.ts delete mode 100644 src/types/dashboard.types.ts delete mode 100644 src/types/errors.ts delete mode 100644 src/types/infra-api.types.ts diff --git a/package.json b/package.json index c47670b10..e24fa2a43 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "db:migrations:create": "bun run scripts:create-migration", "db:migrations:apply": "bun run scripts:apply-migrations", "<<<<<<< Gen": "", - "generate:infra": "bunx openapi-typescript ./spec/openapi.infra.yaml -o ./src/types/infra-api.types.ts", - "generate:dashboard-api": "bunx openapi-typescript ./spec/openapi.dashboard-api.yaml -o ./src/types/dashboard-api.types.ts", - "generate:argus": "bunx openapi-typescript ./spec/openapi.argus.yaml -o ./src/types/argus-api.types.ts", - "generate:supabase": "bunx supabase@latest gen types typescript --schema public > src/types/database.types.ts --project-id $SUPABASE_PROJECT_ID", + "generate:infra": "bunx openapi-typescript ./spec/openapi.infra.yaml -o ./src/core/shared/contracts/infra-api.types.ts", + "generate:dashboard-api": "bunx openapi-typescript ./spec/openapi.dashboard-api.yaml -o ./src/core/shared/contracts/dashboard-api.types.ts", + "generate:argus": "bunx openapi-typescript ./spec/openapi.argus.yaml -o ./src/core/shared/contracts/argus-api.types.ts", + "generate:supabase": "bunx supabase@latest gen types typescript --schema public > src/core/shared/contracts/database.types.ts --project-id $SUPABASE_PROJECT_ID", "<<<<<<< Scripts": "", "scripts:check-app-env": "bun scripts/check-app-env.ts", "scripts:check-e2e-env": "bun scripts/check-e2e-env.ts", diff --git a/src/__test__/unit/chart-utils.test.ts b/src/__test__/unit/chart-utils.test.ts index 6ff740a71..6c68a58fc 100644 --- a/src/__test__/unit/chart-utils.test.ts +++ b/src/__test__/unit/chart-utils.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { transformMetrics } from '@/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils' import { calculateAxisMax } from '@/lib/utils/chart' -import type { ClientTeamMetric } from '@/types/sandboxes.types' +import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' describe('team-metrics-chart-utils', () => { describe('calculateYAxisMax', () => { diff --git a/src/__test__/unit/fill-metrics-with-zeros.test.ts b/src/__test__/unit/fill-metrics-with-zeros.test.ts index 9fd3699e6..ce8bfcbd9 100644 --- a/src/__test__/unit/fill-metrics-with-zeros.test.ts +++ b/src/__test__/unit/fill-metrics-with-zeros.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' -import type { ClientTeamMetrics } from '@/types/sandboxes.types' +import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' describe('fillTeamMetricsWithZeros', () => { describe('Empty data handling', () => { diff --git a/src/app/api/teams/[teamId]/metrics/types.ts b/src/app/api/teams/[teamId]/metrics/types.ts index b1306f467..5a7e8d174 100644 --- a/src/app/api/teams/[teamId]/metrics/types.ts +++ b/src/app/api/teams/[teamId]/metrics/types.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import type { ClientTeamMetrics } from '@/types/sandboxes.types' +import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' export const TeamMetricsRequestSchema = z .object({ diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts b/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts index e2f6a0433..d86f5170b 100644 --- a/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts +++ b/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { ClientSandboxesMetrics } from '@/types/sandboxes.types' +import type { ClientSandboxesMetrics } from '@/core/domains/sandboxes/models.client' export const MetricsRequestSchema = z.object({ sandboxIds: z.array(z.string()).min(1, 'Provide at least one sandbox id'), diff --git a/src/app/api/teams/user/types.ts b/src/app/api/teams/user/types.ts index 60b1fc766..327ec7c70 100644 --- a/src/app/api/teams/user/types.ts +++ b/src/app/api/teams/user/types.ts @@ -1,3 +1,3 @@ -import type { ClientTeam } from '@/types/dashboard.types' +import type { ClientTeam } from '@/core/domains/teams/models' export type UserTeamsResponse = { teams: ClientTeam[] } diff --git a/src/configs/mock-data.ts b/src/configs/mock-data.ts index aa1937a5c..b0fc95ebd 100644 --- a/src/configs/mock-data.ts +++ b/src/configs/mock-data.ts @@ -10,7 +10,7 @@ import type { import type { ClientSandboxesMetrics, ClientTeamMetrics, -} from '@/types/sandboxes.types' +} from '@/core/domains/sandboxes/models.client' const DEFAULT_TEMPLATES: DefaultTemplate[] = [ { diff --git a/src/types/billing.types.ts b/src/core/domains/billing/models.ts similarity index 56% rename from src/types/billing.types.ts rename to src/core/domains/billing/models.ts index e7318dbd1..e79e3233e 100644 --- a/src/types/billing.types.ts +++ b/src/core/domains/billing/models.ts @@ -1,6 +1,4 @@ -import type { ADDON_500_SANDBOXES_ID } from '@/features/dashboard/billing/constants' - -interface Invoice { +export interface Invoice { cost: number paid: boolean url: string @@ -8,16 +6,16 @@ interface Invoice { credits_used: number } -interface BillingLimit { +export interface BillingLimit { limit_amount_gte: number | null alert_amount_gte: number | null } -interface CustomerPortalResponse { +export interface CustomerPortalResponse { url: string } -interface UsageResponse { +export interface UsageResponse { credits: number day_usages: { date: string @@ -37,31 +35,31 @@ interface UsageResponse { }[] } -interface CreateTeamsResponse { +export interface CreateTeamsResponse { id: string slug: string } -interface AddOnOrderItem { - name: typeof ADDON_500_SANDBOXES_ID +export interface AddOnOrderItem { + name: string quantity: number } -interface AddOnOrderCreateResponse { +export interface AddOnOrderCreateResponse { id: string amount_due: number items: AddOnOrderItem[] } -interface AddOnOrderConfirmResponse { +export interface AddOnOrderConfirmResponse { client_secret: string } -interface PaymentMethodsCustomerSession { +export interface PaymentMethodsCustomerSession { client_secret: string } -interface TierLimits { +export interface TierLimits { sandbox_concurrency: number max_cpu: number max_ram_mib: number @@ -69,49 +67,31 @@ interface TierLimits { disk_size_mib: number } -interface TierInfo { +export interface TierInfo { id: string name: string price_cents: number limits?: TierLimits } -interface AddonInfo { - id: typeof ADDON_500_SANDBOXES_ID +export interface AddonInfo { + id: string name: string price_cents: number quantity?: number } -interface TeamAddons { +export interface TeamAddons { current: AddonInfo[] available: AddonInfo[] } -interface TeamTiers { +export interface TeamTiers { current: string available: TierInfo[] } -interface TeamItems { +export interface TeamItems { tiers: TeamTiers addons: TeamAddons } - -export type { - AddonInfo, - AddOnOrderConfirmResponse, - AddOnOrderCreateResponse, - AddOnOrderItem, - BillingLimit, - CreateTeamsResponse, - CustomerPortalResponse, - Invoice, - PaymentMethodsCustomerSession, - TeamAddons, - TeamItems, - TeamTiers, - TierInfo, - TierLimits, - UsageResponse, -} diff --git a/src/core/domains/billing/repository.server.ts b/src/core/domains/billing/repository.server.ts index d1347b6ed..882869548 100644 --- a/src/core/domains/billing/repository.server.ts +++ b/src/core/domains/billing/repository.server.ts @@ -12,7 +12,7 @@ import type { PaymentMethodsCustomerSession, TeamItems, UsageResponse, -} from '@/types/billing.types' +} from '@/core/domains/billing/models' type BillingRepositoryDeps = { billingApiUrl: string diff --git a/src/types/sandboxes.types.ts b/src/core/domains/sandboxes/models.client.ts similarity index 80% rename from src/types/sandboxes.types.ts rename to src/core/domains/sandboxes/models.client.ts index fd3382d9c..0766be2f9 100644 --- a/src/types/sandboxes.types.ts +++ b/src/core/domains/sandboxes/models.client.ts @@ -1,4 +1,4 @@ -import type { TeamMetric } from './api.types' +import type { TeamMetric } from './models' export type ClientSandboxMetric = { cpuCount: number @@ -16,7 +16,7 @@ export type ClientTeamMetric = Pick< TeamMetric, 'concurrentSandboxes' | 'sandboxStartRate' > & { - timestamp: number // unix timestamp in milliseconds + timestamp: number } export type ClientTeamMetrics = Array diff --git a/src/core/domains/sandboxes/models.ts b/src/core/domains/sandboxes/models.ts index 005f79fc4..308c4530c 100644 --- a/src/core/domains/sandboxes/models.ts +++ b/src/core/domains/sandboxes/models.ts @@ -3,6 +3,12 @@ import type { components as DashboardComponents } from '@/types/dashboard-api.ty import type { components as InfraComponents } from '@/types/infra-api.types' export type SandboxLogLevel = InfraComponents['schemas']['LogLevel'] +export type Sandbox = InfraComponents['schemas']['ListedSandbox'] +export type Sandboxes = InfraComponents['schemas']['ListedSandbox'][] +export type SandboxesMetricsRecord = + InfraComponents['schemas']['SandboxesWithMetrics']['sandboxes'] +export type TeamMetric = InfraComponents['schemas']['TeamMetric'] +export type SandboxInfo = InfraComponents['schemas']['SandboxDetail'] interface SandboxDetailsBaseModel { templateID: string diff --git a/src/core/domains/teams/models.ts b/src/core/domains/teams/models.ts index 0d50f07fe..604d5e4ed 100644 --- a/src/core/domains/teams/models.ts +++ b/src/core/domains/teams/models.ts @@ -1,4 +1,9 @@ -export type { ClientTeam } from '@/types/dashboard.types' +import type { Database } from '@/core/shared/contracts/database.types' + +export type ClientTeam = Database['public']['Tables']['teams']['Row'] & { + is_default?: boolean + transformed_default_name?: string +} export type TeamLimits = { concurrentInstances: number diff --git a/src/core/server/actions/auth-actions.ts b/src/core/server/actions/auth-actions.ts index 79407b206..17f0db451 100644 --- a/src/core/server/actions/auth-actions.ts +++ b/src/core/server/actions/auth-actions.ts @@ -17,7 +17,7 @@ import { validateEmail, } from '@/core/server/functions/auth/validate-email' import { verifyTurnstileToken } from '@/lib/captcha/turnstile' -import { actionClient } from '@/lib/clients/action' +import { actionClient } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { relativeUrlSchema } from '@/lib/schemas/url' diff --git a/src/lib/clients/action.ts b/src/core/server/actions/client.ts similarity index 80% rename from src/lib/clients/action.ts rename to src/core/server/actions/client.ts index 8d5e9912b..1c87ae9a0 100644 --- a/src/lib/clients/action.ts +++ b/src/core/server/actions/client.ts @@ -11,11 +11,11 @@ import { import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' -import { UnauthenticatedError, UnknownError } from '@/types/errors' -import { ActionError, flattenClientInputValue } from '../utils/action' -import { l } from './logger/logger' -import { createClient } from './supabase/server' -import { getTracer } from './tracer' +import { UnauthenticatedError, UnknownError } from '@/core/shared/errors' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' +import { getTracer } from '@/core/shared/clients/tracer' +import { ActionError, flattenClientInputValue } from './utils' export const actionClient = createSafeActionClient({ handleServerError(e) { @@ -24,7 +24,6 @@ export const actionClient = createSafeActionClient({ s?.setStatus({ code: SpanStatusCode.ERROR }) s?.recordException(e) - // part of our strategy how to leak errors to a user if (e instanceof ActionError) { return e.message } @@ -82,7 +81,6 @@ export const actionClient = createSafeActionClient({ server_function_name: name, server_function_input: clientInput, server_function_duration_ms: duration.toFixed(3), - team_id: flattenClientInputValue(clientInput, 'teamId'), template_id: flattenClientInputValue(clientInput, 'templateId'), sandbox_id: flattenClientInputValue(clientInput, 'sandboxId'), @@ -143,19 +141,12 @@ export const actionClient = createSafeActionClient({ export const authActionClient = actionClient.use(async ({ next }) => { const supabase = await createClient() - - // retrieve session from storage medium (cookies) - // if no stored session found, not authenticated - - // it's fine to use the "insecure" cookie session here, since we only use it for quick denial and do a proper auth check (auth.getUser) afterwards. const session = await getSessionInsecure(supabase) - // early return if user is no session already if (!session) { throw UnauthenticatedError() } - // now retrieve user from supabase to use further const { data: { user }, } = await getUserByToken(session.access_token) @@ -180,27 +171,6 @@ export const authActionClient = actionClient.use(async ({ next }) => { }) }) -/** - * Middleware that automatically resolves team ID from teamIdOrSlug. - * - * This middleware: - * 1. Requires that the client input contains a 'teamIdOrSlug' property - * 2. Resolves the teamIdOrSlug to an actual team ID using getTeamIdFromSegmentMemo - * 3. Throws unauthorized() if the team ID cannot be resolved (team doesn't exist or user lacks access) - * 4. Adds the resolved teamId to the context for use in the action handler - * 5. Throws an error if no teamIdOrSlug is provided - * - * @example - * ```ts - * const myAction = authActionClient - * .use(withTeamIdResolution) - * .schema(z.object({ teamIdOrSlug: z.string(), ... })) - * .action(async ({ parsedInput, ctx }) => { - * // ctx.teamId is now available and guaranteed to be valid - * const { teamId } = ctx - * }) - * ``` - */ export const withTeamIdResolution = createMiddleware<{ ctx: { user: User diff --git a/src/core/server/actions/key-actions.ts b/src/core/server/actions/key-actions.ts index 606e7a99c..fe50f360f 100644 --- a/src/core/server/actions/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -3,7 +3,7 @@ import { revalidatePath, updateTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' diff --git a/src/core/server/actions/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts index c01315e4b..1c6fed5f9 100644 --- a/src/core/server/actions/sandbox-actions.ts +++ b/src/core/server/actions/sandbox-actions.ts @@ -4,7 +4,7 @@ import { updateTag } from 'next/cache' import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/server/actions/team-actions.ts b/src/core/server/actions/team-actions.ts index 1023b881b..e3face12d 100644 --- a/src/core/server/actions/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -13,12 +13,12 @@ import { UpdateTeamNameSchema, } from '@/core/domains/teams/schemas' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError, returnServerError } from '@/lib/utils/action' -import type { CreateTeamsResponse } from '@/types/billing.types' +import type { CreateTeamsResponse } from '@/core/domains/billing/models' export const updateTeamNameAction = authActionClient .schema(UpdateTeamNameSchema) diff --git a/src/core/server/actions/user-actions.ts b/src/core/server/actions/user-actions.ts index dd230abd5..5ee67e6ff 100644 --- a/src/core/server/actions/user-actions.ts +++ b/src/core/server/actions/user-actions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from 'next/cache' import { headers } from 'next/headers' import { returnValidationErrors } from 'next-safe-action' import { z } from 'zod' -import { authActionClient } from '@/lib/clients/action' +import { authActionClient } from '@/core/server/actions/client' import { generateE2BUserAccessToken } from '@/lib/utils/server' const UpdateUserSchema = z diff --git a/src/core/server/actions/utils.ts b/src/core/server/actions/utils.ts new file mode 100644 index 000000000..09956ca9e --- /dev/null +++ b/src/core/server/actions/utils.ts @@ -0,0 +1,36 @@ +import { UnauthorizedError, UnknownError } from '@/core/shared/errors' + +export class ActionError extends Error { + constructor(message: string) { + super(message) + this.name = 'ActionError' + } +} + +export const returnServerError = (message: string) => { + throw new ActionError(message) +} + +export function handleDefaultInfraError(status: number) { + switch (status) { + case 403: + return returnServerError( + 'You may have reached your billing limits or your account may be blocked. Please check your billing settings or contact support.' + ) + case 401: + return returnServerError(UnauthorizedError('Unauthorized').message) + default: + return returnServerError(UnknownError().message) + } +} + +export const flattenClientInputValue = ( + clientInput: unknown, + key: string +): string | undefined => { + if (typeof clientInput === 'object' && clientInput && key in clientInput) { + return clientInput[key as keyof typeof clientInput] + } + + return undefined +} diff --git a/src/core/server/actions/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts index 7744334fb..0e7eac442 100644 --- a/src/core/server/actions/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -8,7 +8,7 @@ import { UpdateWebhookSecretSchema, UpsertWebhookSchema, } from '@/core/server/functions/webhooks/schema' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' diff --git a/src/core/server/functions/keys/get-api-keys.ts b/src/core/server/functions/keys/get-api-keys.ts index 97993b255..3c6c17dc1 100644 --- a/src/core/server/functions/keys/get-api-keys.ts +++ b/src/core/server/functions/keys/get-api-keys.ts @@ -3,7 +3,7 @@ import 'server-only' import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' diff --git a/src/core/server/functions/sandboxes/get-team-metrics-core.ts b/src/core/server/functions/sandboxes/get-team-metrics-core.ts index 3633c4df7..50b12684f 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-core.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-core.ts @@ -11,7 +11,7 @@ import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/util import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' -import type { ClientTeamMetrics } from '@/types/sandboxes.types' +import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' interface GetTeamMetricsCoreParams { accessToken: string diff --git a/src/core/server/functions/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts index 74d629e6d..c624c54aa 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-max.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-max.ts @@ -5,7 +5,7 @@ import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { USE_MOCK_DATA } from '@/configs/flags' import { MOCK_TEAM_METRICS_MAX_DATA } from '@/configs/mock-data' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/server/functions/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts index ec99273a7..3bbc5660b 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics.ts @@ -2,7 +2,7 @@ import 'server-only' import { z } from 'zod' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' import { getTeamMetricsCore } from './get-team-metrics-core' diff --git a/src/core/server/functions/sandboxes/utils.ts b/src/core/server/functions/sandboxes/utils.ts index 96e5622b7..e2546a87c 100644 --- a/src/core/server/functions/sandboxes/utils.ts +++ b/src/core/server/functions/sandboxes/utils.ts @@ -1,9 +1,9 @@ import { TEAM_METRICS_BACKEND_COLLECTION_INTERVAL_MS } from '@/configs/intervals' -import type { SandboxesMetricsRecord } from '@/types/api.types' +import type { SandboxesMetricsRecord } from '@/core/domains/sandboxes/models' import type { ClientSandboxesMetrics, ClientTeamMetrics, -} from '@/types/sandboxes.types' +} from '@/core/domains/sandboxes/models.client' export function transformMetricsToClientMetrics( metrics: SandboxesMetricsRecord diff --git a/src/core/server/functions/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts index 0b4a7f1a1..4fab0da80 100644 --- a/src/core/server/functions/team/get-team-limits.ts +++ b/src/core/server/functions/team/get-team-limits.ts @@ -3,7 +3,7 @@ import 'server-only' import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' export interface TeamLimits { diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts index 8b45d8684..8ed29eb76 100644 --- a/src/core/server/functions/team/get-team-members.ts +++ b/src/core/server/functions/team/get-team-members.ts @@ -2,7 +2,7 @@ import 'server-only' import { z } from 'zod' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' const GetTeamMembersSchema = z.object({ diff --git a/src/core/server/functions/team/get-team.ts b/src/core/server/functions/team/get-team.ts index 3baba86ce..7d1b32086 100644 --- a/src/core/server/functions/team/get-team.ts +++ b/src/core/server/functions/team/get-team.ts @@ -2,7 +2,7 @@ import 'server-cli-only' import { z } from 'zod' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts index f10430aef..53f4b0138 100644 --- a/src/core/server/functions/usage/get-usage.ts +++ b/src/core/server/functions/usage/get-usage.ts @@ -3,7 +3,7 @@ import 'server-only' import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' diff --git a/src/core/server/functions/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts index 9ebe63fb9..e394e687c 100644 --- a/src/core/server/functions/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -1,7 +1,7 @@ import 'server-only' import { z } from 'zod' -import { authActionClient, withTeamIdResolution } from '@/lib/clients/action' +import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' diff --git a/src/lib/clients/api.ts b/src/core/shared/clients/api.ts similarity index 80% rename from src/lib/clients/api.ts rename to src/core/shared/clients/api.ts index 60b906e96..181446a68 100644 --- a/src/lib/clients/api.ts +++ b/src/core/shared/clients/api.ts @@ -1,7 +1,7 @@ import createClient from 'openapi-fetch' -import type { paths as ArgusPaths } from '@/types/argus-api.types' -import type { paths as DashboardPaths } from '@/types/dashboard-api.types' -import type { paths as InfraPaths } from '@/types/infra-api.types' +import type { paths as ArgusPaths } from '@/core/shared/contracts/argus-api.types' +import type { paths as DashboardPaths } from '@/core/shared/contracts/dashboard-api.types' +import type { paths as InfraPaths } from '@/core/shared/contracts/infra-api.types' type CombinedPaths = InfraPaths & ArgusPaths diff --git a/src/lib/clients/kv.ts b/src/core/shared/clients/kv.ts similarity index 100% rename from src/lib/clients/kv.ts rename to src/core/shared/clients/kv.ts diff --git a/src/lib/clients/logger/logger.node.ts b/src/core/shared/clients/logger/logger.node.ts similarity index 100% rename from src/lib/clients/logger/logger.node.ts rename to src/core/shared/clients/logger/logger.node.ts diff --git a/src/lib/clients/logger/logger.ts b/src/core/shared/clients/logger/logger.ts similarity index 64% rename from src/lib/clients/logger/logger.ts rename to src/core/shared/clients/logger/logger.ts index 8bbe83528..e41ef67d7 100644 --- a/src/lib/clients/logger/logger.ts +++ b/src/core/shared/clients/logger/logger.ts @@ -1,48 +1,21 @@ -/** - * Universal logger that picks the correct implementation for the current runtime - * (Node, Edge, Browser) and exposes an API compatible with `pino`. - * - * In Node & Browser we return the real pino instance. - * In Edge we fall back to the minimal JSON logger implemented in `logger.edge.ts`. - */ - import pino, { type Logger } from 'pino' import type { ErrorObject } from 'serialize-error' -/** - * Represents platform-specific metadata that can be included in logs. - * These are top-level properties that provide structured metadata for log filtering and analysis. - */ interface PlatformContextKeys { - /** Identifier for the team */ team_id?: string - /** Identifier for the user */ user_id?: string - /** Identifier for the sandbox */ - sandbox_id?: string - /** Identifier for the template */ template_id?: string } -/** - * Context data structure for logging entries. - * Extends platform-specific metadata with additional fields. - */ interface ILoggerContext extends Record, PlatformContextKeys { - /** Key to help identify log entry in-code */ key: string - - /** Should contain Error */ error?: ErrorObject | unknown - - /** Should contain context around the log */ context?: Record } interface ILogger { child(bindings: Record): Logger - fatal(context: ILoggerContext, message?: string, ...args: unknown[]): void error(context: ILoggerContext, message?: string, ...args: unknown[]): void warn(context: ILoggerContext, message?: string, ...args: unknown[]): void @@ -88,6 +61,5 @@ const createLogger = () => { } export const logger: ILogger = createLogger() - export const l = logger export default logger diff --git a/src/lib/clients/meter.ts b/src/core/shared/clients/meter.ts similarity index 100% rename from src/lib/clients/meter.ts rename to src/core/shared/clients/meter.ts diff --git a/src/lib/clients/storage.ts b/src/core/shared/clients/storage.ts similarity index 67% rename from src/lib/clients/storage.ts rename to src/core/shared/clients/storage.ts index 91618c883..a33877528 100644 --- a/src/lib/clients/storage.ts +++ b/src/core/shared/clients/storage.ts @@ -2,19 +2,12 @@ import type { FileObject } from '@supabase/storage-js' import { STORAGE_BUCKET_NAME } from '@/configs/storage' import { supabaseAdmin } from './supabase/admin' -/** - * Upload a file to Supabase Storage - * @param fileBuffer - The file buffer to upload - * @param destination - The destination path in the bucket - * @param contentType - The content type of the file - * @returns The public URL of the uploaded file - */ export async function uploadFile( fileBuffer: Buffer, destination: string, contentType: string ): Promise { - const { data, error } = await supabaseAdmin.storage + const { error } = await supabaseAdmin.storage .from(STORAGE_BUCKET_NAME) .upload(destination, fileBuffer, { contentType, @@ -33,11 +26,6 @@ export async function uploadFile( return urlData.publicUrl } -/** - * Get a list of files from Supabase Storage - * @param folderPath - The path of the folder in the bucket - * @returns The list of files - */ export async function getFiles(folderPath: string): Promise { const { data, error } = await supabaseAdmin.storage .from(STORAGE_BUCKET_NAME) @@ -51,10 +39,7 @@ export async function getFiles(folderPath: string): Promise { return data } -/** - * Delete a file from Supabase Storage - * @param filePath - The path of the file in the bucket - */ + export async function deleteFile(filePath: string): Promise { const { error } = await supabaseAdmin.storage .from(STORAGE_BUCKET_NAME) @@ -65,12 +50,6 @@ export async function deleteFile(filePath: string): Promise { } } -/** - * Get a signed URL for a file (for temporary access) - * @param filePath - The path of the file in the bucket - * @param expiresInMinutes - How long the URL should be valid for (in minutes) - * @returns The signed URL - */ export async function getSignedUrl( filePath: string, expiresInMinutes = 15 diff --git a/src/lib/clients/supabase/admin.ts b/src/core/shared/clients/supabase/admin.ts similarity index 81% rename from src/lib/clients/supabase/admin.ts rename to src/core/shared/clients/supabase/admin.ts index 08df51274..0d9b8a887 100644 --- a/src/lib/clients/supabase/admin.ts +++ b/src/core/shared/clients/supabase/admin.ts @@ -1,7 +1,7 @@ import 'server-cli-only' import { createClient } from '@supabase/supabase-js' -import type { Database } from '@/types/database.types' +import type { Database } from '@/core/shared/contracts/database.types' export const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, diff --git a/src/lib/clients/supabase/client.ts b/src/core/shared/clients/supabase/client.ts similarity index 74% rename from src/lib/clients/supabase/client.ts rename to src/core/shared/clients/supabase/client.ts index fad5b74c2..565be79c4 100644 --- a/src/lib/clients/supabase/client.ts +++ b/src/core/shared/clients/supabase/client.ts @@ -1,7 +1,7 @@ 'use client' import { createBrowserClient } from '@supabase/ssr' -import type { Database } from '@/types/database.types' +import type { Database } from '@/core/shared/contracts/database.types' export const supabase = createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL, diff --git a/src/lib/clients/supabase/server.ts b/src/core/shared/clients/supabase/server.ts similarity index 69% rename from src/lib/clients/supabase/server.ts rename to src/core/shared/clients/supabase/server.ts index a1a9fe844..d82092614 100644 --- a/src/lib/clients/supabase/server.ts +++ b/src/core/shared/clients/supabase/server.ts @@ -1,6 +1,6 @@ import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' -import type { Database } from '@/types/database.types' +import type { Database } from '@/core/shared/contracts/database.types' export const createClient = async () => { const cookieStore = await cookies() @@ -18,11 +18,7 @@ export const createClient = async () => { cookiesToSet.forEach(({ name, value, options }) => { cookieStore.set(name, value, options) }) - } catch (error) { - // The `set` method was called from a Server Component. - // This can be ignored since we have middleware refreshing - // user sessions. - } + } catch (_error) {} }, }, } diff --git a/src/lib/clients/tracer.ts b/src/core/shared/clients/tracer.ts similarity index 100% rename from src/lib/clients/tracer.ts rename to src/core/shared/clients/tracer.ts diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts new file mode 100644 index 000000000..fb40ff39f --- /dev/null +++ b/src/core/shared/contracts/argus-api.types.ts @@ -0,0 +1,479 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Request was successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/sandboxes/{sandboxID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get sandbox events */ + get: { + parameters: { + query?: { + offset?: number; + limit?: number; + orderAsc?: boolean; + }; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxEvent"][]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/sandboxes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get all sandbox events for the team associated with the API key */ + get: { + parameters: { + query?: { + offset?: number; + limit?: number; + orderAsc?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxEvent"][]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/webhooks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List registered webhooks. */ + get: operations["webhooksList"]; + put?: never; + /** @description Register events webhook. */ + post: operations["webhookCreate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/events/webhooks/{webhookID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get a registered webhook. */ + get: operations["webhookGet"]; + put?: never; + post?: never; + /** @description Delete a registered webhook. */ + delete: operations["webhookDelete"]; + options?: never; + head?: never; + /** @description Update a registered webhook configuration. */ + patch: operations["webhookUpdate"]; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + /** + * Format: int32 + * @description Error code + */ + code: number; + /** @description Error */ + message: string; + }; + /** @description Sandbox event */ + SandboxEvent: { + /** + * Format: uuid + * @description Event unique identifier + */ + id: string; + /** @description Event structure version */ + version: string; + /** @description Event name */ + type: string; + /** + * @deprecated + * @description Category of the event (e.g., 'lifecycle', 'process', etc.) + */ + eventCategory?: string; + /** + * @deprecated + * @description Label for the specific event type (e.g., 'sandbox_started', 'process_oom', etc.) + */ + eventLabel?: string; + /** @description Optional JSON data associated with the event */ + eventData?: Record | null; + /** + * Format: date-time + * @description Timestamp of the event + */ + timestamp: string; + /** + * Format: string + * @description Unique identifier for the sandbox + */ + sandboxId: string; + /** + * Format: string + * @description Unique identifier for the sandbox execution + */ + sandboxExecutionId: string; + /** + * Format: string + * @description Unique identifier for the sandbox template + */ + sandboxTemplateId: string; + /** + * Format: string + * @description Unique identifier for the sandbox build + */ + sandboxBuildId: string; + /** + * Format: uuid + * @description Team identifier associated with the sandbox + */ + sandboxTeamId: string; + }; + /** @description Configuration for registering new webhooks */ + WebhookCreate: { + name: string; + /** Format: uri */ + url: string; + events: string[]; + /** @default true */ + enabled: boolean; + /** @description Secret used to sign the webhook payloads */ + signatureSecret: string; + }; + /** @description Webhook creation response */ + WebhookCreation: { + /** @description Webhook unique identifier */ + id: string; + /** @description Webhook user friendly name */ + name: string; + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string; + /** @description Unique identifier for the team */ + teamId: string; + /** Format: uri */ + url: string; + enabled: boolean; + events: string[]; + }; + /** @description Webhook detail response */ + WebhookDetail: { + /** @description Webhook unique identifier */ + id: string; + /** @description Unique identifier for the team */ + teamId: string; + /** @description Webhook user friendly name */ + name: string; + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string; + /** Format: uri */ + url: string; + enabled: boolean; + events: string[]; + }; + /** @description Configuration for updating existing webhooks */ + WebhookConfiguration: { + enabled?: boolean; + /** @description Webhook user friendly name */ + name?: string; + /** Format: uri */ + url?: string; + events?: string[]; + /** @description Secret used to sign the webhook payloads */ + signatureSecret?: string; + }; + }; + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: { + sandboxID: string; + webhookID: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + webhooksList: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of registered webhooks. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookDetail"][]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + webhookCreate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WebhookCreate"]; + }; + }; + responses: { + /** @description Successfully created webhook. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookCreation"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + webhookGet: { + parameters: { + query?: never; + header?: never; + path: { + webhookID: components["parameters"]["webhookID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the webhook configuration. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookDetail"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + webhookDelete: { + parameters: { + query?: never; + header?: never; + path: { + webhookID: components["parameters"]["webhookID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully deleted webhook. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + webhookUpdate: { + parameters: { + query?: never; + header?: never; + path: { + webhookID: components["parameters"]["webhookID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WebhookConfiguration"]; + }; + }; + responses: { + /** @description Successfully updated webhook. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookDetail"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; +} diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts new file mode 100644 index 000000000..f1528f078 --- /dev/null +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -0,0 +1,795 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Health check successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List team builds */ + get: { + parameters: { + query?: { + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template?: components["parameters"]["build_id_or_template"]; + /** @description Comma-separated list of build statuses to include. */ + statuses?: components["parameters"]["build_statuses"]; + /** @description Maximum number of items to return per page. */ + limit?: components["parameters"]["builds_limit"]; + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + cursor?: components["parameters"]["builds_cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned paginated builds. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildsListResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds/statuses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get build statuses */ + get: { + parameters: { + query: { + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: components["parameters"]["build_ids"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned build statuses */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildsStatusesResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/builds/{build_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get build details */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the build. */ + build_id: components["parameters"]["build_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned build details. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BuildInfo"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/record": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get sandbox record */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the sandbox. */ + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned sandbox details. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxRecord"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List user teams + * @description Returns all teams the authenticated user belongs to, with limits and default flag. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned user teams. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserTeamsResponse"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/resolve": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Resolve team identity + * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. + */ + get: { + parameters: { + query: { + /** @description Team slug to resolve. */ + slug: components["parameters"]["teamSlug"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully resolved team. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamResolveResponse"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update team */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTeamRequest"]; + }; + }; + responses: { + /** @description Successfully updated team. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateTeamResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + trace?: never; + }; + "/teams/{teamId}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List team members */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned team members. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMembersResponse"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** Add team member */ + post: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AddTeamMemberRequest"]; + }; + }; + responses: { + /** @description Successfully added team member. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/members/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Remove team member */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Identifier of the team. */ + teamId: components["parameters"]["teamId"]; + /** @description Identifier of the user. */ + userId: components["parameters"]["userId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully removed team member. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/defaults": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List default templates + * @description Returns the list of default templates with their latest build info and aliases. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned default templates. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DefaultTemplatesResponse"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + /** + * Format: int32 + * @description Error code. + */ + code: number; + /** @description Error message. */ + message: string; + }; + /** + * @description Build status mapped for dashboard clients. + * @enum {string} + */ + BuildStatus: "building" | "failed" | "success"; + ListedBuild: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string; + /** @description Template alias when present, otherwise template ID. */ + template: string; + /** @description Identifier of the template. */ + templateId: string; + status: components["schemas"]["BuildStatus"]; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + }; + BuildsListResponse: { + data: components["schemas"]["ListedBuild"][]; + /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ + nextCursor: string | null; + }; + BuildStatusItem: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string; + status: components["schemas"]["BuildStatus"]; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + }; + BuildsStatusesResponse: { + /** @description List of build statuses */ + buildStatuses: components["schemas"]["BuildStatusItem"][]; + }; + BuildInfo: { + /** @description Template names related to this build, if available. */ + names?: string[] | null; + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string; + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null; + status: components["schemas"]["BuildStatus"]; + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null; + }; + /** + * Format: int64 + * @description CPU cores for the sandbox + */ + CPUCount: number; + /** + * Format: int64 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number; + /** + * Format: int64 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number; + SandboxRecord: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string; + /** @description Alias of the template */ + alias?: string; + /** @description Identifier of the sandbox */ + sandboxID: string; + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string; + /** + * Format: date-time + * @description Time when the sandbox was stopped + */ + stoppedAt?: string | null; + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + }; + HealthResponse: { + /** @description Human-readable health check result. */ + message: string; + }; + UserTeamLimits: { + /** Format: int64 */ + maxLengthHours: number; + /** Format: int32 */ + concurrentSandboxes: number; + /** Format: int32 */ + concurrentTemplateBuilds: number; + /** Format: int32 */ + maxVcpu: number; + /** Format: int32 */ + maxRamMb: number; + /** Format: int32 */ + diskMb: number; + }; + UserTeam: { + /** Format: uuid */ + id: string; + name: string; + slug: string; + tier: string; + email: string; + isDefault: boolean; + limits: components["schemas"]["UserTeamLimits"]; + }; + UserTeamsResponse: { + teams: components["schemas"]["UserTeam"][]; + }; + TeamMember: { + /** Format: uuid */ + id: string; + email: string; + isDefault: boolean; + /** Format: uuid */ + addedBy?: string | null; + /** Format: date-time */ + createdAt: string | null; + }; + TeamMembersResponse: { + members: components["schemas"]["TeamMember"][]; + }; + UpdateTeamRequest: { + name: string; + }; + UpdateTeamResponse: { + /** Format: uuid */ + id: string; + name: string; + }; + AddTeamMemberRequest: { + /** Format: email */ + email: string; + }; + DefaultTemplateAlias: { + alias: string; + namespace?: string | null; + }; + DefaultTemplate: { + id: string; + aliases: components["schemas"]["DefaultTemplateAlias"][]; + /** Format: uuid */ + buildId: string; + /** Format: int64 */ + ramMb: number; + /** Format: int64 */ + vcpu: number; + /** Format: int64 */ + totalDiskSizeMb: number | null; + envdVersion?: string | null; + /** Format: date-time */ + createdAt: string; + public: boolean; + /** Format: int32 */ + buildCount: number; + /** Format: int64 */ + spawnCount: number; + }; + DefaultTemplatesResponse: { + templates: components["schemas"]["DefaultTemplate"][]; + }; + TeamResolveResponse: { + /** Format: uuid */ + id: string; + slug: string; + }; + }; + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: { + /** @description Identifier of the build. */ + build_id: string; + /** @description Identifier of the sandbox. */ + sandboxID: string; + /** @description Maximum number of items to return per page. */ + builds_limit: number; + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + builds_cursor: string; + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template: string; + /** @description Comma-separated list of build statuses to include. */ + build_statuses: components["schemas"]["BuildStatus"][]; + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: string[]; + /** @description Identifier of the team. */ + teamId: string; + /** @description Identifier of the user. */ + userId: string; + /** @description Team slug to resolve. */ + teamSlug: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/src/types/database.types.ts b/src/core/shared/contracts/database.types.ts similarity index 100% rename from src/types/database.types.ts rename to src/core/shared/contracts/database.types.ts diff --git a/src/core/shared/contracts/infra-api.types.ts b/src/core/shared/contracts/infra-api.types.ts new file mode 100644 index 000000000..4e4e31757 --- /dev/null +++ b/src/core/shared/contracts/infra-api.types.ts @@ -0,0 +1,3219 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Request was successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all teams */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all teams */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Team"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamID}/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get metrics for the team */ + get: { + parameters: { + query?: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number; + end?: number; + }; + header?: never; + path: { + teamID: components["parameters"]["teamID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the team metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMetric"][]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamID}/metrics/max": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get the maximum metrics for the team in the given interval */ + get: { + parameters: { + query: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number; + end?: number; + /** @description Metric to retrieve the maximum value for */ + metric: "concurrent_sandboxes" | "sandbox_start_rate"; + }; + header?: never; + path: { + teamID: components["parameters"]["teamID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the team metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MaxTeamMetric"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all running sandboxes */ + get: { + parameters: { + query?: { + /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ + metadata?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all running sandboxes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListedSandbox"][]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** @description Create a sandbox from the template */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewSandbox"]; + }; + }; + responses: { + /** @description The sandbox was created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/sandboxes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all sandboxes */ + get: { + parameters: { + query?: { + /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ + metadata?: string; + /** @description Filter sandboxes by one or more states */ + state?: components["schemas"]["SandboxState"][]; + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all running sandboxes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListedSandbox"][]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List metrics for given sandboxes */ + get: { + parameters: { + query: { + /** @description Comma-separated list of sandbox IDs to get metrics for */ + sandbox_ids: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all running sandboxes with metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxesWithMetrics"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * @deprecated + * @description Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. + */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + start?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; + }; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox logs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxLogs"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/sandboxes/{sandboxID}/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get sandbox logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; + /** @description Direction of the logs that should be returned */ + direction?: components["schemas"]["LogsDirection"]; + /** @description Minimum log level to return. Logs below this level are excluded */ + level?: components["schemas"]["LogLevel"]; + /** @description Case-sensitive substring match on log message content */ + search?: string; + }; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox logs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxLogsV2Response"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get a sandbox by id */ + get: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxDetail"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + /** @description Kill a sandbox */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The sandbox was killed successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get sandbox metrics */ + get: { + parameters: { + query?: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number; + end?: number; + }; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the sandbox metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxMetric"][]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/pause": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Pause the sandbox */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The sandbox was paused successfully and can be resumed */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 409: components["responses"]["409"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/resume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @deprecated + * @description Resume the sandbox + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResumedSandbox"]; + }; + }; + responses: { + /** @description The sandbox was resumed successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 409: components["responses"]["409"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/connect": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConnectSandbox"]; + }; + }; + responses: { + /** @description The sandbox was already running */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + /** @description The sandbox was resumed successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/timeout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Set the timeout for the sandbox. The sandbox will expire x seconds from the time of the request. Calling this method multiple times overwrites the TTL, each time using the current timestamp as the starting point to measure the timeout duration. */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * Format: int32 + * @description Timeout in seconds from the current time after which the sandbox should expire + */ + timeout: number; + }; + }; + }; + responses: { + /** @description Successfully set the sandbox timeout */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/refreshes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Refresh the sandbox extending its time to live */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** @description Duration for which the sandbox should be kept alive in seconds */ + duration?: number; + }; + }; + }; + responses: { + /** @description Successfully refreshed the sandbox */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxID}/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. */ + name?: string; + }; + }; + }; + responses: { + /** @description Snapshot created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SnapshotInfo"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all snapshots for the team */ + get: { + parameters: { + query?: { + sandboxID?: string; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned snapshots */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SnapshotInfo"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v3/templates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a new template */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildRequestV3"]; + }; + }; + responses: { + /** @description The build was requested successfully */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateRequestResponseV3"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/templates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @deprecated + * @description Create a new template + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildRequestV2"]; + }; + }; + responses: { + /** @description The build was requested successfully */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateLegacy"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/files/{hash}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get an upload link for a tar file containing build layer files */ + get: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + hash: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The upload link where to upload the tar file */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateBuildFileUpload"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all templates */ + get: { + parameters: { + query?: { + teamID?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all templates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Template"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** + * @deprecated + * @description Create a new template + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildRequest"]; + }; + }; + responses: { + /** @description The build was accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateLegacy"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all builds for a template */ + get: { + parameters: { + query?: { + /** @description Cursor to start the list from */ + nextToken?: components["parameters"]["paginationNextToken"]; + /** @description Maximum number of items to return per page */ + limit?: components["parameters"]["paginationLimit"]; + }; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template with its builds */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateWithBuilds"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** + * @deprecated + * @description Rebuild an template + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildRequest"]; + }; + }; + responses: { + /** @description The build was accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateLegacy"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + /** @description Delete a template */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The template was deleted successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + /** + * @deprecated + * @description Update template + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateUpdateRequest"]; + }; + }; + responses: { + /** @description The template was updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + trace?: never; + }; + "/templates/{templateID}/builds/{buildID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * @deprecated + * @description Start the build + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The build has started */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/templates/{templateID}/builds/{buildID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Start the build */ + post: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateBuildStartV2"]; + }; + }; + responses: { + /** @description The build has started */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/templates/{templateID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** @description Update template */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TemplateUpdateRequest"]; + }; + }; + responses: { + /** @description The template was updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateUpdateResponse"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + trace?: never; + }; + "/templates/{templateID}/builds/{buildID}/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get template build info */ + get: { + parameters: { + query?: { + /** @description Index of the starting build log that should be returned with the template */ + logsOffset?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; + level?: components["schemas"]["LogLevel"]; + }; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateBuildInfo"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/builds/{buildID}/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get template build logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number; + /** @description Maximum number of logs that should be returned */ + limit?: number; + direction?: components["schemas"]["LogsDirection"]; + level?: components["schemas"]["LogLevel"]; + /** @description Source of the logs that should be returned from */ + source?: components["schemas"]["LogsSource"]; + }; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + buildID: components["parameters"]["buildID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template build logs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateBuildLogsResponse"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/tags": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Assign tag(s) to a template build */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AssignTemplateTagsRequest"]; + }; + }; + responses: { + /** @description Tag assigned successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssignedTemplateTags"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + /** @description Delete multiple tags from templates */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; + }; + }; + responses: { + /** @description Tags deleted successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/{templateID}/tags": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all tags for a template */ + get: { + parameters: { + query?: never; + header?: never; + path: { + templateID: components["parameters"]["templateID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the template tags */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateTag"][]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/templates/aliases/{alias}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Check if template with given alias exists */ + get: { + parameters: { + query?: never; + header?: never; + path: { + alias: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully queried template by alias */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TemplateAliasResponse"]; + }; + }; + 400: components["responses"]["400"]; + 403: components["responses"]["403"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/nodes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all nodes */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all nodes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Node"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/nodes/{nodeID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get node info */ + get: { + parameters: { + query?: { + /** @description Identifier of the cluster */ + clusterID?: string; + }; + header?: never; + path: { + nodeID: components["parameters"]["nodeID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned the node */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NodeDetail"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** @description Change status of a node */ + post: { + parameters: { + query?: never; + header?: never; + path: { + nodeID: components["parameters"]["nodeID"]; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["NodeStatusChange"]; + }; + }; + responses: { + /** @description The node status was changed successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/teams/{teamID}/sandboxes/kill": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Kill all sandboxes for a team + * @description Kills all sandboxes for the specified team + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Team ID */ + teamID: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully killed sandboxes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdminSandboxKillResult"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/teams/{teamID}/builds/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel all builds for a team + * @description Cancels all in-progress and pending builds for the specified team + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Team ID */ + teamID: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully cancelled builds */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdminBuildCancelResult"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/access-tokens": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a new access token */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewAccessToken"]; + }; + }; + responses: { + /** @description Access token created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreatedAccessToken"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/access-tokens/{accessTokenID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** @description Delete an access token */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + accessTokenID: components["parameters"]["accessTokenID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Access token deleted successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all team API keys */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully returned all team API keys */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamAPIKey"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** @description Create a new team API key */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewTeamAPIKey"]; + }; + }; + responses: { + /** @description Team API key created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreatedTeamAPIKey"]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api-keys/{apiKeyID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** @description Delete a team API key */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + apiKeyID: components["parameters"]["apiKeyID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Team API key deleted successfully */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + /** @description Update a team API key */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + apiKeyID: components["parameters"]["apiKeyID"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTeamAPIKey"]; + }; + }; + responses: { + /** @description Team API key updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + trace?: never; + }; + "/volumes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all team volumes */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully listed all team volumes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Volume"][]; + }; + }; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + /** @description Create a new team volume */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewVolume"]; + }; + }; + responses: { + /** @description Successfully created a new team volume */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["VolumeAndToken"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 500: components["responses"]["500"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/volumes/{volumeID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Get team volume info */ + get: { + parameters: { + query?: never; + header?: never; + path: { + volumeID: components["parameters"]["volumeID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved a team volume */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["VolumeAndToken"]; + }; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + put?: never; + post?: never; + /** @description Delete a team volume */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + volumeID: components["parameters"]["volumeID"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully deleted a team volume */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 500: components["responses"]["500"]; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Team: { + /** @description Identifier of the team */ + teamID: string; + /** @description Name of the team */ + name: string; + /** @description API key for the team */ + apiKey: string; + /** @description Whether the team is the default team */ + isDefault: boolean; + }; + TeamUser: { + /** + * Format: uuid + * @description Identifier of the user + */ + id: string; + /** @description Email of the user */ + email: string; + }; + TemplateUpdateRequest: { + /** @description Whether the template is public or only accessible by the team */ + public?: boolean; + }; + TemplateUpdateResponse: { + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[]; + }; + /** + * Format: int32 + * @description CPU cores for the sandbox + */ + CPUCount: number; + /** + * Format: int32 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number; + /** + * Format: int32 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number; + /** @description Version of the envd running in the sandbox */ + EnvdVersion: string; + SandboxMetadata: { + [key: string]: string; + }; + /** + * @description State of the sandbox + * @enum {string} + */ + SandboxState: "running" | "paused"; + SnapshotInfo: { + /** @description Identifier of the snapshot template including the tag. Uses namespace/alias when a name was provided (e.g. team-slug/my-snapshot:default), otherwise falls back to the raw template ID (e.g. abc123:default). */ + snapshotID: string; + /** @description Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) */ + names: string[]; + }; + EnvVars: { + [key: string]: string; + }; + /** @description MCP configuration for the sandbox */ + Mcp: { + [key: string]: unknown; + } | null; + SandboxNetworkConfig: { + /** + * @description Specify if the sandbox URLs should be accessible only with authentication. + * @default true + */ + allowPublicTraffic: boolean; + /** @description List of allowed CIDR blocks or IP addresses for egress traffic. Allowed addresses always take precedence over blocked addresses. */ + allowOut?: string[]; + /** @description List of denied CIDR blocks or IP addresses for egress traffic */ + denyOut?: string[]; + /** @description Specify host mask which will be used for all sandbox requests */ + maskRequestHost?: string; + }; + /** + * @description Auto-resume enabled flag for paused sandboxes. Default false. + * @default false + */ + SandboxAutoResumeEnabled: boolean; + /** @description Auto-resume configuration for paused sandboxes. */ + SandboxAutoResumeConfig: { + enabled: components["schemas"]["SandboxAutoResumeEnabled"]; + }; + /** @description Log entry with timestamp and line */ + SandboxLog: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string; + /** @description Log line content */ + line: string; + }; + SandboxLogEntry: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string; + /** @description Log message content */ + message: string; + level: components["schemas"]["LogLevel"]; + fields: { + [key: string]: string; + }; + }; + SandboxLogs: { + /** @description Logs of the sandbox */ + logs: components["schemas"]["SandboxLog"][]; + /** @description Structured logs of the sandbox */ + logEntries: components["schemas"]["SandboxLogEntry"][]; + }; + SandboxLogsV2Response: { + /** + * @description Sandbox logs structured + * @default [] + */ + logs: components["schemas"]["SandboxLogEntry"][]; + }; + /** @description Metric entry with timestamp and line */ + SandboxMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string; + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number; + /** + * Format: int32 + * @description Number of CPU cores + */ + cpuCount: number; + /** + * Format: float + * @description CPU usage percentage + */ + cpuUsedPct: number; + /** + * Format: int64 + * @description Memory used in bytes + */ + memUsed: number; + /** + * Format: int64 + * @description Total memory in bytes + */ + memTotal: number; + /** + * Format: int64 + * @description Disk used in bytes + */ + diskUsed: number; + /** + * Format: int64 + * @description Total disk space in bytes + */ + diskTotal: number; + }; + SandboxVolumeMount: { + /** @description Name of the volume */ + name: string; + /** @description Path of the volume */ + path: string; + }; + Sandbox: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string; + /** @description Identifier of the sandbox */ + sandboxID: string; + /** @description Alias of the template */ + alias?: string; + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string; + envdVersion: components["schemas"]["EnvdVersion"]; + /** @description Access token used for envd communication */ + envdAccessToken?: string; + /** @description Token required for accessing sandbox via proxy. */ + trafficAccessToken?: string | null; + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null; + }; + SandboxDetail: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string; + /** @description Alias of the template */ + alias?: string; + /** @description Identifier of the sandbox */ + sandboxID: string; + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string; + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string; + /** + * Format: date-time + * @description Time when the sandbox will expire + */ + endAt: string; + envdVersion: components["schemas"]["EnvdVersion"]; + /** @description Access token used for envd communication */ + envdAccessToken?: string; + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + metadata?: components["schemas"]["SandboxMetadata"]; + state: components["schemas"]["SandboxState"]; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; + }; + ListedSandbox: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string; + /** @description Alias of the template */ + alias?: string; + /** @description Identifier of the sandbox */ + sandboxID: string; + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string; + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string; + /** + * Format: date-time + * @description Time when the sandbox will expire + */ + endAt: string; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + metadata?: components["schemas"]["SandboxMetadata"]; + state: components["schemas"]["SandboxState"]; + envdVersion: components["schemas"]["EnvdVersion"]; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; + }; + SandboxesWithMetrics: { + sandboxes: { + [key: string]: components["schemas"]["SandboxMetric"]; + }; + }; + NewSandbox: { + /** @description Identifier of the required template */ + templateID: string; + /** + * Format: int32 + * @description Time to live for the sandbox in seconds. + * @default 15 + */ + timeout: number; + /** + * @description Automatically pauses the sandbox after the timeout + * @default false + */ + autoPause: boolean; + autoResume?: components["schemas"]["SandboxAutoResumeConfig"]; + /** @description Secure all system communication with sandbox */ + secure?: boolean; + /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ + allow_internet_access?: boolean; + network?: components["schemas"]["SandboxNetworkConfig"]; + metadata?: components["schemas"]["SandboxMetadata"]; + envVars?: components["schemas"]["EnvVars"]; + mcp?: components["schemas"]["Mcp"]; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; + }; + ResumedSandbox: { + /** + * Format: int32 + * @description Time to live for the sandbox in seconds. + * @default 15 + */ + timeout: number; + /** + * @deprecated + * @description Automatically pauses the sandbox after the timeout + */ + autoPause?: boolean; + }; + ConnectSandbox: { + /** + * Format: int32 + * @description Timeout in seconds from the current time after which the sandbox should expire + */ + timeout: number; + }; + /** @description Team metric with timestamp */ + TeamMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string; + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number; + /** + * Format: int32 + * @description The number of concurrent sandboxes for the team + */ + concurrentSandboxes: number; + /** + * Format: float + * @description Number of sandboxes started per second + */ + sandboxStartRate: number; + }; + /** @description Team metric with timestamp */ + MaxTeamMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string; + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number; + /** @description The maximum value of the requested metric in the given interval */ + value: number; + }; + AdminSandboxKillResult: { + /** @description Number of sandboxes successfully killed */ + killedCount: number; + /** @description Number of sandboxes that failed to kill */ + failedCount: number; + }; + AdminBuildCancelResult: { + /** @description Number of builds successfully cancelled */ + cancelledCount: number; + /** @description Number of builds that failed to cancel */ + failedCount: number; + }; + VolumeToken: { + token: string; + }; + Template: { + /** @description Identifier of the template */ + templateID: string; + /** @description Identifier of the last successful build for given template */ + buildID: string; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + /** @description Whether the template is public or only accessible by the team */ + public: boolean; + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[]; + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[]; + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string; + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string; + createdBy: components["schemas"]["TeamUser"] | null; + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null; + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number; + /** + * Format: int32 + * @description Number of times the template was built + */ + buildCount: number; + envdVersion: components["schemas"]["EnvdVersion"]; + buildStatus: components["schemas"]["TemplateBuildStatus"]; + }; + TemplateRequestResponseV3: { + /** @description Identifier of the template */ + templateID: string; + /** @description Identifier of the last successful build for given template */ + buildID: string; + /** @description Whether the template is public or only accessible by the team */ + public: boolean; + /** @description Names of the template */ + names: string[]; + /** @description Tags assigned to the template build */ + tags: string[]; + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[]; + }; + TemplateLegacy: { + /** @description Identifier of the template */ + templateID: string; + /** @description Identifier of the last successful build for given template */ + buildID: string; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB: components["schemas"]["DiskSizeMB"]; + /** @description Whether the template is public or only accessible by the team */ + public: boolean; + /** @description Aliases of the template */ + aliases: string[]; + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string; + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string; + createdBy: components["schemas"]["TeamUser"] | null; + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null; + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number; + /** + * Format: int32 + * @description Number of times the template was built + */ + buildCount: number; + envdVersion: components["schemas"]["EnvdVersion"]; + }; + TemplateBuild: { + /** + * Format: uuid + * @description Identifier of the build + */ + buildID: string; + status: components["schemas"]["TemplateBuildStatus"]; + /** + * Format: date-time + * @description Time when the build was created + */ + createdAt: string; + /** + * Format: date-time + * @description Time when the build was last updated + */ + updatedAt: string; + /** + * Format: date-time + * @description Time when the build was finished + */ + finishedAt?: string; + cpuCount: components["schemas"]["CPUCount"]; + memoryMB: components["schemas"]["MemoryMB"]; + diskSizeMB?: components["schemas"]["DiskSizeMB"]; + envdVersion?: components["schemas"]["EnvdVersion"]; + }; + TemplateWithBuilds: { + /** @description Identifier of the template */ + templateID: string; + /** @description Whether the template is public or only accessible by the team */ + public: boolean; + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[]; + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[]; + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string; + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string; + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null; + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number; + /** @description List of builds for the template */ + builds: components["schemas"]["TemplateBuild"][]; + }; + TemplateAliasResponse: { + /** @description Identifier of the template */ + templateID: string; + /** @description Whether the template is public or only accessible by the team */ + public: boolean; + }; + TemplateBuildRequest: { + /** @description Alias of the template */ + alias?: string; + /** @description Dockerfile for the template */ + dockerfile: string; + /** @description Identifier of the team */ + teamID?: string; + /** @description Start command to execute in the template after the build */ + startCmd?: string; + /** @description Ready check command to execute in the template after the build */ + readyCmd?: string; + cpuCount?: components["schemas"]["CPUCount"]; + memoryMB?: components["schemas"]["MemoryMB"]; + }; + /** @description Step in the template build process */ + TemplateStep: { + /** @description Type of the step */ + type: string; + /** + * @description Arguments for the step + * @default [] + */ + args: string[]; + /** @description Hash of the files used in the step */ + filesHash?: string; + /** + * @description Whether the step should be forced to run regardless of the cache + * @default false + */ + force: boolean; + }; + TemplateBuildRequestV3: { + /** @description Name of the template. Can include a tag with colon separator (e.g. "my-template" or "my-template:v1"). If tag is included, it will be treated as if the tag was provided in the tags array. */ + name?: string; + /** @description Tags to assign to the template build */ + tags?: string[]; + /** + * @deprecated + * @description Alias of the template. Deprecated, use name instead. + */ + alias?: string; + /** + * @deprecated + * @description Identifier of the team + */ + teamID?: string; + cpuCount?: components["schemas"]["CPUCount"]; + memoryMB?: components["schemas"]["MemoryMB"]; + }; + TemplateBuildRequestV2: { + /** @description Alias of the template */ + alias: string; + /** + * @deprecated + * @description Identifier of the team + */ + teamID?: string; + cpuCount?: components["schemas"]["CPUCount"]; + memoryMB?: components["schemas"]["MemoryMB"]; + }; + FromImageRegistry: components["schemas"]["AWSRegistry"] | components["schemas"]["GCPRegistry"] | components["schemas"]["GeneralRegistry"]; + AWSRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "aws"; + /** @description AWS Access Key ID for ECR authentication */ + awsAccessKeyId: string; + /** @description AWS Secret Access Key for ECR authentication */ + awsSecretAccessKey: string; + /** @description AWS Region where the ECR registry is located */ + awsRegion: string; + }; + GCPRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "gcp"; + /** @description Service Account JSON for GCP authentication */ + serviceAccountJson: string; + }; + GeneralRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "registry"; + /** @description Username to use for the registry */ + username: string; + /** @description Password to use for the registry */ + password: string; + }; + TemplateBuildStartV2: { + /** @description Image to use as a base for the template build */ + fromImage?: string; + /** @description Template to use as a base for the template build */ + fromTemplate?: string; + fromImageRegistry?: components["schemas"]["FromImageRegistry"]; + /** + * @description Whether the whole build should be forced to run regardless of the cache + * @default false + */ + force: boolean; + /** + * @description List of steps to execute in the template build + * @default [] + */ + steps: components["schemas"]["TemplateStep"][]; + /** @description Start command to execute in the template after the build */ + startCmd?: string; + /** @description Ready check command to execute in the template after the build */ + readyCmd?: string; + }; + TemplateBuildFileUpload: { + /** @description Whether the file is already present in the cache */ + present: boolean; + /** @description Url where the file should be uploaded to */ + url?: string; + }; + /** + * @description State of the sandbox + * @enum {string} + */ + LogLevel: "debug" | "info" | "warn" | "error"; + BuildLogEntry: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string; + /** @description Log message content */ + message: string; + level: components["schemas"]["LogLevel"]; + /** @description Step in the build process related to the log entry */ + step?: string; + }; + BuildStatusReason: { + /** @description Message with the status reason, currently reporting only for error status */ + message: string; + /** @description Step that failed */ + step?: string; + /** + * @description Log entries related to the status reason + * @default [] + */ + logEntries: components["schemas"]["BuildLogEntry"][]; + }; + /** + * @description Status of the template build + * @enum {string} + */ + TemplateBuildStatus: "building" | "waiting" | "ready" | "error"; + TemplateBuildInfo: { + /** + * @description Build logs + * @default [] + */ + logs: string[]; + /** + * @description Build logs structured + * @default [] + */ + logEntries: components["schemas"]["BuildLogEntry"][]; + /** @description Identifier of the template */ + templateID: string; + /** @description Identifier of the build */ + buildID: string; + status: components["schemas"]["TemplateBuildStatus"]; + reason?: components["schemas"]["BuildStatusReason"]; + }; + TemplateBuildLogsResponse: { + /** + * @description Build logs structured + * @default [] + */ + logs: components["schemas"]["BuildLogEntry"][]; + }; + /** + * @description Direction of the logs that should be returned + * @enum {string} + */ + LogsDirection: "forward" | "backward"; + /** + * @description Source of the logs that should be returned + * @enum {string} + */ + LogsSource: "temporary" | "persistent"; + /** + * @description Status of the node + * @enum {string} + */ + NodeStatus: "ready" | "draining" | "connecting" | "unhealthy"; + NodeStatusChange: { + /** + * Format: uuid + * @description Identifier of the cluster + */ + clusterID?: string; + status: components["schemas"]["NodeStatus"]; + }; + DiskMetrics: { + /** @description Mount point of the disk */ + mountPoint: string; + /** @description Device name */ + device: string; + /** @description Filesystem type (e.g., ext4, xfs) */ + filesystemType: string; + /** + * Format: uint64 + * @description Used space in bytes + */ + usedBytes: number; + /** + * Format: uint64 + * @description Total space in bytes + */ + totalBytes: number; + }; + /** @description Node metrics */ + NodeMetrics: { + /** + * Format: uint32 + * @description Number of allocated CPU cores + */ + allocatedCPU: number; + /** + * Format: uint32 + * @description Node CPU usage percentage + */ + cpuPercent: number; + /** + * Format: uint32 + * @description Total number of CPU cores on the node + */ + cpuCount: number; + /** + * Format: uint64 + * @description Amount of allocated memory in bytes + */ + allocatedMemoryBytes: number; + /** + * Format: uint64 + * @description Node memory used in bytes + */ + memoryUsedBytes: number; + /** + * Format: uint64 + * @description Total node memory in bytes + */ + memoryTotalBytes: number; + /** @description Detailed metrics for each disk/mount point */ + disks: components["schemas"]["DiskMetrics"][]; + }; + MachineInfo: { + /** @description CPU family of the node */ + cpuFamily: string; + /** @description CPU model of the node */ + cpuModel: string; + /** @description CPU model name of the node */ + cpuModelName: string; + /** @description CPU architecture of the node */ + cpuArchitecture: string; + }; + Node: { + /** @description Version of the orchestrator */ + version: string; + /** @description Commit of the orchestrator */ + commit: string; + /** @description Identifier of the node */ + id: string; + /** @description Service instance identifier of the node */ + serviceInstanceID: string; + /** @description Identifier of the cluster */ + clusterID: string; + machineInfo: components["schemas"]["MachineInfo"]; + status: components["schemas"]["NodeStatus"]; + /** + * Format: uint32 + * @description Number of sandboxes running on the node + */ + sandboxCount: number; + metrics: components["schemas"]["NodeMetrics"]; + /** + * Format: uint64 + * @description Number of sandbox create successes + */ + createSuccesses: number; + /** + * Format: uint64 + * @description Number of sandbox create fails + */ + createFails: number; + /** + * Format: int + * @description Number of starting Sandboxes + */ + sandboxStartingCount: number; + }; + NodeDetail: { + /** @description Identifier of the cluster */ + clusterID: string; + /** @description Version of the orchestrator */ + version: string; + /** @description Commit of the orchestrator */ + commit: string; + /** @description Identifier of the node */ + id: string; + /** @description Service instance identifier of the node */ + serviceInstanceID: string; + machineInfo: components["schemas"]["MachineInfo"]; + status: components["schemas"]["NodeStatus"]; + /** + * Format: uint32 + * @description Number of sandboxes running on the node + */ + sandboxCount: number; + metrics: components["schemas"]["NodeMetrics"]; + /** @description List of cached builds id on the node */ + cachedBuilds: string[]; + /** + * Format: uint64 + * @description Number of sandbox create successes + */ + createSuccesses: number; + /** + * Format: uint64 + * @description Number of sandbox create fails + */ + createFails: number; + }; + CreatedAccessToken: { + /** + * Format: uuid + * @description Identifier of the access token + */ + id: string; + /** @description Name of the access token */ + name: string; + /** @description The fully created access token */ + token: string; + mask: components["schemas"]["IdentifierMaskingDetails"]; + /** + * Format: date-time + * @description Timestamp of access token creation + */ + createdAt: string; + }; + NewAccessToken: { + /** @description Name of the access token */ + name: string; + }; + TeamAPIKey: { + /** + * Format: uuid + * @description Identifier of the API key + */ + id: string; + /** @description Name of the API key */ + name: string; + mask: components["schemas"]["IdentifierMaskingDetails"]; + /** + * Format: date-time + * @description Timestamp of API key creation + */ + createdAt: string; + createdBy?: components["schemas"]["TeamUser"] | null; + /** + * Format: date-time + * @description Last time this API key was used + */ + lastUsed?: string | null; + }; + CreatedTeamAPIKey: { + /** + * Format: uuid + * @description Identifier of the API key + */ + id: string; + /** @description Raw value of the API key */ + key: string; + mask: components["schemas"]["IdentifierMaskingDetails"]; + /** @description Name of the API key */ + name: string; + /** + * Format: date-time + * @description Timestamp of API key creation + */ + createdAt: string; + createdBy?: components["schemas"]["TeamUser"] | null; + /** + * Format: date-time + * @description Last time this API key was used + */ + lastUsed?: string | null; + }; + NewTeamAPIKey: { + /** @description Name of the API key */ + name: string; + }; + UpdateTeamAPIKey: { + /** @description New name for the API key */ + name: string; + }; + AssignedTemplateTags: { + /** @description Assigned tags of the template */ + tags: string[]; + /** + * Format: uuid + * @description Identifier of the build associated with these tags + */ + buildID: string; + }; + TemplateTag: { + /** @description The tag name */ + tag: string; + /** + * Format: uuid + * @description Identifier of the build associated with this tag + */ + buildID: string; + /** + * Format: date-time + * @description Time when the tag was assigned + */ + createdAt: string; + }; + AssignTemplateTagsRequest: { + /** @description Target template in "name:tag" format */ + target: string; + /** @description Tags to assign to the template */ + tags: string[]; + }; + DeleteTemplateTagsRequest: { + /** @description Name of the template */ + name: string; + /** @description Tags to delete */ + tags: string[]; + }; + Error: { + /** + * Format: int32 + * @description Error code + */ + code: number; + /** @description Error */ + message: string; + }; + IdentifierMaskingDetails: { + /** @description Prefix that identifies the token or key type */ + prefix: string; + /** @description Length of the token or key */ + valueLength: number; + /** @description Prefix used in masked version of the token or key */ + maskedValuePrefix: string; + /** @description Suffix used in masked version of the token or key */ + maskedValueSuffix: string; + }; + Volume: { + /** @description ID of the volume */ + volumeID: string; + /** @description Name of the volume */ + name: string; + }; + VolumeAndToken: { + /** @description ID of the volume */ + volumeID: string; + /** @description Name of the volume */ + name: string; + /** @description Auth token to use for interacting with volume content */ + token: string; + }; + NewVolume: { + /** @description Name of the volume */ + name: string; + }; + }; + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: { + templateID: string; + buildID: string; + sandboxID: string; + teamID: string; + nodeID: string; + apiKeyID: string; + accessTokenID: string; + snapshotID: string; + tag: string; + /** @description Maximum number of items to return per page */ + paginationLimit: number; + /** @description Cursor to start the list from */ + paginationNextToken: string; + volumeID: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/src/core/shared/errors.ts b/src/core/shared/errors.ts index fc97fd5a1..c7c6b0c34 100644 --- a/src/core/shared/errors.ts +++ b/src/core/shared/errors.ts @@ -1,5 +1,45 @@ import type { RepoError, RepoErrorCode } from './result' +export type E2BErrorCode = + | 'UNAUTHENTICATED' + | 'UNAUTHORIZED' + | 'INVALID_PARAMETERS' + | 'INTERNAL_SERVER_ERROR' + | 'API_ERROR' + | 'UNKNOWN' + | string + +export class E2BError extends Error { + public code: E2BErrorCode + + constructor(code: E2BErrorCode, message: string) { + super(message) + this.name = 'E2BError' + this.code = code + } +} + +export const UnauthenticatedError = () => + new E2BError('UNAUTHENTICATED', 'User not authenticated') + +export const UnauthorizedError = (message: string) => + new E2BError('UNAUTHORIZED', message) + +export const InvalidApiKeyError = (message: string) => + new E2BError('INVALID_API_KEY', message) + +export const InvalidParametersError = (message: string) => + new E2BError('INVALID_PARAMETERS', message) + +export const ApiError = (message: string) => new E2BError('API_ERROR', message) + +export const UnknownError = (message?: string) => + new E2BError( + 'UNKNOWN', + message ?? + 'An Unexpected Error Occurred, please try again. If the problem persists, please contact support.' + ) + export function createRepoError(input: { code: RepoErrorCode status: number diff --git a/src/core/shared/schemas/api.ts b/src/core/shared/schemas/api.ts new file mode 100644 index 000000000..0440ef1b1 --- /dev/null +++ b/src/core/shared/schemas/api.ts @@ -0,0 +1,7 @@ +import z from 'zod' + +export const SandboxIdSchema = z + .string() + .min(1, 'Sandbox ID is required') + .max(100, 'Sandbox ID too long') + .regex(/^[a-z0-9]+$/, 'Invalid sandbox ID format') diff --git a/src/core/shared/schemas/team.ts b/src/core/shared/schemas/team.ts new file mode 100644 index 000000000..66c312573 --- /dev/null +++ b/src/core/shared/schemas/team.ts @@ -0,0 +1,3 @@ +import { z } from 'zod' + +export const TeamIdOrSlugSchema = z.union([z.uuid(), z.string()]) diff --git a/src/lib/schemas/url.ts b/src/core/shared/schemas/url.ts similarity index 100% rename from src/lib/schemas/url.ts rename to src/core/shared/schemas/url.ts diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index f67873ca5..177464ce9 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -8,7 +8,7 @@ import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' import { useTRPC } from '@/trpc/client' -import type { AddonInfo } from '@/types/billing.types' +import type { AddonInfo } from '@/core/domains/billing/models' import HelpTooltip from '@/ui/help-tooltip' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/billing/select-plan.tsx b/src/features/dashboard/billing/select-plan.tsx index e6b3f6de9..01546caac 100644 --- a/src/features/dashboard/billing/select-plan.tsx +++ b/src/features/dashboard/billing/select-plan.tsx @@ -6,7 +6,7 @@ import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' import { useTRPC } from '@/trpc/client' -import type { TierInfo } from '@/types/billing.types' +import type { TierInfo } from '@/core/domains/billing/models' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' import { diff --git a/src/features/dashboard/billing/types.ts b/src/features/dashboard/billing/types.ts index 1df167ddb..ae49131ff 100644 --- a/src/features/dashboard/billing/types.ts +++ b/src/features/dashboard/billing/types.ts @@ -1,5 +1,5 @@ import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' -import type { TeamItems } from '@/types/billing.types' +import type { TeamItems } from '@/core/domains/billing/models' export interface BillingData { items: TeamItems diff --git a/src/features/dashboard/billing/utils.ts b/src/features/dashboard/billing/utils.ts index 7768efa4c..4b60b1385 100644 --- a/src/features/dashboard/billing/utils.ts +++ b/src/features/dashboard/billing/utils.ts @@ -1,5 +1,5 @@ import { l } from '@/lib/clients/logger/logger' -import type { TeamItems } from '@/types/billing.types' +import type { TeamItems } from '@/core/domains/billing/models' import { ADDON_500_SANDBOXES_ID, TIER_BASE_ID, TIER_PRO_ID } from './constants' import type { BillingAddonData, BillingTierData } from './types' diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index ec8ec4505..d590777bb 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -2,7 +2,7 @@ import type { User } from '@supabase/supabase-js' import { createContext, type ReactNode, useContext, useState } from 'react' -import type { ClientTeam } from '@/types/dashboard.types' +import type { ClientTeam } from '@/core/domains/teams/models' interface DashboardContextValue { team: ClientTeam diff --git a/src/features/dashboard/limits/alert-card.tsx b/src/features/dashboard/limits/alert-card.tsx index c5fbdbb69..dcc164f11 100644 --- a/src/features/dashboard/limits/alert-card.tsx +++ b/src/features/dashboard/limits/alert-card.tsx @@ -1,7 +1,7 @@ 'use client' import { useRouteParams } from '@/lib/hooks/use-route-params' -import type { BillingLimit } from '@/types/billing.types' +import type { BillingLimit } from '@/core/domains/billing/models' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' import LimitForm from './limit-form' diff --git a/src/features/dashboard/limits/limit-card.tsx b/src/features/dashboard/limits/limit-card.tsx index 39bad9af3..6bd6b5228 100644 --- a/src/features/dashboard/limits/limit-card.tsx +++ b/src/features/dashboard/limits/limit-card.tsx @@ -1,7 +1,7 @@ 'use client' import { useRouteParams } from '@/lib/hooks/use-route-params' -import type { BillingLimit } from '@/types/billing.types' +import type { BillingLimit } from '@/core/domains/billing/models' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' import LimitForm from './limit-form' diff --git a/src/features/dashboard/sandboxes/list/stores/metrics-store.ts b/src/features/dashboard/sandboxes/list/stores/metrics-store.ts index d436ab01f..ea605a401 100644 --- a/src/features/dashboard/sandboxes/list/stores/metrics-store.ts +++ b/src/features/dashboard/sandboxes/list/stores/metrics-store.ts @@ -1,7 +1,7 @@ 'use client' import { create } from 'zustand' -import type { ClientSandboxesMetrics } from '@/types/sandboxes.types' +import type { ClientSandboxesMetrics } from '@/core/domains/sandboxes/models.client' // maximum number of sandbox metrics to keep in memory // this is to prevent the store from growing too large and causing performance issues diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts index 75dc33868..21a426074 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts @@ -1,4 +1,4 @@ -import type { ClientTeamMetric } from '@/types/sandboxes.types' +import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' export type ChartType = 'concurrent' | 'start-rate' diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts index d058b18aa..f64c276a5 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts @@ -1,5 +1,5 @@ import { formatAxisNumber } from '@/lib/utils/formatting' -import type { ClientTeamMetric } from '@/types/sandboxes.types' +import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' import type { TeamMetricDataPoint } from './types' /** diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index 270910aaa..7d1f19550 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -4,7 +4,7 @@ import { useCallback } from 'react' import useSWR from 'swr' import type { UserTeamsResponse } from '@/app/api/teams/user/types' import { useTeamCookieManager } from '@/lib/hooks/use-team' -import type { ClientTeam } from '@/types/dashboard.types' +import type { ClientTeam } from '@/core/domains/teams/models' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { DropdownMenuItem, diff --git a/src/features/dashboard/usage/sampling-utils.ts b/src/features/dashboard/usage/sampling-utils.ts index 4fd35581d..6084ac04f 100644 --- a/src/features/dashboard/usage/sampling-utils.ts +++ b/src/features/dashboard/usage/sampling-utils.ts @@ -1,5 +1,5 @@ import { startOfISOWeek } from 'date-fns' -import type { UsageResponse } from '@/types/billing.types' +import type { UsageResponse } from '@/core/domains/billing/models' import { HOURLY_SAMPLING_THRESHOLD_DAYS, WEEKLY_SAMPLING_THRESHOLD_DAYS, diff --git a/src/features/dashboard/usage/usage-charts-context.tsx b/src/features/dashboard/usage/usage-charts-context.tsx index 93ede8ee4..36c34c515 100644 --- a/src/features/dashboard/usage/usage-charts-context.tsx +++ b/src/features/dashboard/usage/usage-charts-context.tsx @@ -10,7 +10,7 @@ import { useState, } from 'react' import { fillTimeSeriesWithEmptyPoints } from '@/lib/utils/time-series' -import type { UsageResponse } from '@/types/billing.types' +import type { UsageResponse } from '@/core/domains/billing/models' import { INITIAL_TIMEFRAME_FALLBACK_RANGE_MS } from './constants' import { calculateTotals, diff --git a/src/lib/hooks/use-team.ts b/src/lib/hooks/use-team.ts index 542056b5c..80c35e101 100644 --- a/src/lib/hooks/use-team.ts +++ b/src/lib/hooks/use-team.ts @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { useDebounceCallback } from 'usehooks-ts' import { useDashboard } from '@/features/dashboard/context' -import type { ClientTeam } from '@/types/dashboard.types' +import type { ClientTeam } from '@/core/domains/teams/models' export const useTeamCookieManager = () => { const { team } = useDashboard() diff --git a/src/lib/hooks/use-user.ts b/src/lib/hooks/use-user.ts index 7c616124c..72462a2bd 100644 --- a/src/lib/hooks/use-user.ts +++ b/src/lib/hooks/use-user.ts @@ -2,7 +2,7 @@ import type { User } from '@supabase/supabase-js' import useSWR from 'swr' -import { supabase } from '../clients/supabase/client' +import { supabase } from '@/core/shared/clients/supabase/client' interface UseUserProps { initialData?: User diff --git a/src/lib/schemas/api.ts b/src/lib/schemas/api.ts deleted file mode 100644 index 12d038c72..000000000 --- a/src/lib/schemas/api.ts +++ /dev/null @@ -1,13 +0,0 @@ -import z from 'zod' - -/** - * Sandbox ID validation schema - * Accepts standard sandbox ID format (alphanumeric characters) - * Maximum length of 100 characters to prevent DoS attacks - * Example: i08krhnahpx21arf83wmz - */ -export const SandboxIdSchema = z - .string() - .min(1, 'Sandbox ID is required') - .max(100, 'Sandbox ID too long') - .regex(/^[a-z0-9]+$/, 'Invalid sandbox ID format') diff --git a/src/lib/schemas/team.ts b/src/lib/schemas/team.ts deleted file mode 100644 index 1ffe1164e..000000000 --- a/src/lib/schemas/team.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod' - -export const TeamIdOrSlugSchema = z.union([ - z.uuid(), - z.string(), - // FIXME: Add correct team regex as in db slug generation - // .regex( - // /^[a-z0-9]+(-[a-z0-9]+)*$/i, - // 'Must be a valid slug (words separated by hyphens)' - // ), -]) diff --git a/src/lib/utils/action.ts b/src/lib/utils/action.ts deleted file mode 100644 index 14e5b4135..000000000 --- a/src/lib/utils/action.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { UnauthorizedError, UnknownError } from '@/types/errors' - -/** - * Custom error class for action-specific errors. - * - * @remarks - * This error class is used in server actions but will be serialized and sent to the client. - * Be careful not to include sensitive information in error messages as they will be exposed to the client. - * When thrown in a server action, the message will be visible in client-side error handling. - */ -export class ActionError extends Error { - constructor(message: string) { - super(message) - this.name = 'ActionError' - } -} - -/** - * Returns a server error to the client by throwing an ActionError. - * - * @param message - The error message to be sent to the client - * @returns Never returns as it always throws an error - * - * @example - * ```ts - * if (error) { - * if (error.code === 'invalid_credentials') { - * return returnServerError('Invalid credentials') - * } - * throw error - * } - * ``` - * - * @remarks - * This function is used to return user-friendly error messages from server actions. - * The error message will be serialized and sent to the client, so avoid including - * sensitive information. - */ -export const returnServerError = (message: string) => { - throw new ActionError(message) -} - -export function handleDefaultInfraError(status: number) { - switch (status) { - case 403: - return returnServerError( - 'You may have reached your billing limits or your account may be blocked. Please check your billing settings or contact support.' - ) - case 401: - return returnServerError(UnauthorizedError('Unauthorized').message) - default: - return returnServerError(UnknownError().message) - } -} - -export const flattenClientInputValue = ( - clientInput: unknown, - key: string -): string | undefined => { - if (typeof clientInput === 'object' && clientInput && key in clientInput) { - return clientInput[key as keyof typeof clientInput] - } - - return undefined -} diff --git a/src/lib/utils/rewrites.ts b/src/lib/utils/rewrites.ts index e354cbce9..c1aaba52d 100644 --- a/src/lib/utils/rewrites.ts +++ b/src/lib/utils/rewrites.ts @@ -6,7 +6,7 @@ import { ROUTE_REWRITE_CONFIG, } from '@/configs/rewrites' import type { RewriteConfig } from '@/types/rewrites.types' -import { l } from '../clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' function getRewriteForPath( path: string, diff --git a/src/lib/utils/server.ts b/src/lib/utils/server.ts index 8a9bd7107..f1780accc 100644 --- a/src/lib/utils/server.ts +++ b/src/lib/utils/server.ts @@ -5,9 +5,9 @@ import { cache } from 'react' import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' -import { infra } from '../clients/api' -import { l } from '../clients/logger/logger' -import { returnServerError } from './action' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' +import { returnServerError } from '@/core/server/actions/utils' /* * This function generates an e2b user access token for a given user. diff --git a/src/proxy.ts b/src/proxy.ts index ab6b4f841..d64c5c2dc 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { ALLOW_SEO_INDEXING } from './configs/flags' import { getAuthRedirect } from './core/server/http/proxy' -import { l } from './lib/clients/logger/logger' +import { l } from './core/shared/clients/logger/logger' import { getMiddlewareRedirectFromPath } from './lib/utils/redirects' import { getRewriteForPath } from './lib/utils/rewrites' diff --git a/src/types/argus-api.types.ts b/src/types/argus-api.types.ts deleted file mode 100644 index b9286818e..000000000 --- a/src/types/argus-api.types.ts +++ /dev/null @@ -1,479 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/health': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Health check */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Request was successful */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/events/sandboxes/{sandboxID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get sandbox events */ - get: { - parameters: { - query?: { - offset?: number - limit?: number - orderAsc?: boolean - } - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox events */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxEvent'][] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/events/sandboxes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get all sandbox events for the team associated with the API key */ - get: { - parameters: { - query?: { - offset?: number - limit?: number - orderAsc?: boolean - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox events */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxEvent'][] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/events/webhooks': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List registered webhooks. */ - get: operations['webhooksList'] - put?: never - /** @description Register events webhook. */ - post: operations['webhookCreate'] - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/events/webhooks/{webhookID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get a registered webhook. */ - get: operations['webhookGet'] - put?: never - post?: never - /** @description Delete a registered webhook. */ - delete: operations['webhookDelete'] - options?: never - head?: never - /** @description Update a registered webhook configuration. */ - patch: operations['webhookUpdate'] - trace?: never - } -} -export type webhooks = Record -export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code - */ - code: number - /** @description Error */ - message: string - } - /** @description Sandbox event */ - SandboxEvent: { - /** - * Format: uuid - * @description Event unique identifier - */ - id: string - /** @description Event structure version */ - version: string - /** @description Event name */ - type: string - /** - * @deprecated - * @description Category of the event (e.g., 'lifecycle', 'process', etc.) - */ - eventCategory?: string - /** - * @deprecated - * @description Label for the specific event type (e.g., 'sandbox_started', 'process_oom', etc.) - */ - eventLabel?: string - /** @description Optional JSON data associated with the event */ - eventData?: Record | null - /** - * Format: date-time - * @description Timestamp of the event - */ - timestamp: string - /** - * Format: string - * @description Unique identifier for the sandbox - */ - sandboxId: string - /** - * Format: string - * @description Unique identifier for the sandbox execution - */ - sandboxExecutionId: string - /** - * Format: string - * @description Unique identifier for the sandbox template - */ - sandboxTemplateId: string - /** - * Format: string - * @description Unique identifier for the sandbox build - */ - sandboxBuildId: string - /** - * Format: uuid - * @description Team identifier associated with the sandbox - */ - sandboxTeamId: string - } - /** @description Configuration for registering new webhooks */ - WebhookCreate: { - name: string - /** Format: uri */ - url: string - events: string[] - /** @default true */ - enabled: boolean - /** @description Secret used to sign the webhook payloads */ - signatureSecret: string - } - /** @description Webhook creation response */ - WebhookCreation: { - /** @description Webhook unique identifier */ - id: string - /** @description Webhook user friendly name */ - name: string - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string - /** @description Unique identifier for the team */ - teamId: string - /** Format: uri */ - url: string - enabled: boolean - events: string[] - } - /** @description Webhook detail response */ - WebhookDetail: { - /** @description Webhook unique identifier */ - id: string - /** @description Unique identifier for the team */ - teamId: string - /** @description Webhook user friendly name */ - name: string - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string - /** Format: uri */ - url: string - enabled: boolean - events: string[] - } - /** @description Configuration for updating existing webhooks */ - WebhookConfiguration: { - enabled?: boolean - /** @description Webhook user friendly name */ - name?: string - /** Format: uri */ - url?: string - events?: string[] - /** @description Secret used to sign the webhook payloads */ - signatureSecret?: string - } - } - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - } - parameters: { - sandboxID: string - webhookID: string - } - requestBodies: never - headers: never - pathItems: never -} -export type $defs = Record -export interface operations { - webhooksList: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description List of registered webhooks. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['WebhookDetail'][] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - webhookCreate: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['WebhookCreate'] - } - } - responses: { - /** @description Successfully created webhook. */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['WebhookCreation'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - webhookGet: { - parameters: { - query?: never - header?: never - path: { - webhookID: components['parameters']['webhookID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the webhook configuration. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['WebhookDetail'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - webhookDelete: { - parameters: { - query?: never - header?: never - path: { - webhookID: components['parameters']['webhookID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully deleted webhook. */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - webhookUpdate: { - parameters: { - query?: never - header?: never - path: { - webhookID: components['parameters']['webhookID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['WebhookConfiguration'] - } - } - responses: { - /** @description Successfully updated webhook. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['WebhookDetail'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } -} diff --git a/src/types/dashboard-api.types.ts b/src/types/dashboard-api.types.ts deleted file mode 100644 index be5848fcc..000000000 --- a/src/types/dashboard-api.types.ts +++ /dev/null @@ -1,795 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/health': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Health check */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Health check successful */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['HealthResponse'] - } - } - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** List team builds */ - get: { - parameters: { - query?: { - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template?: components['parameters']['build_id_or_template'] - /** @description Comma-separated list of build statuses to include. */ - statuses?: components['parameters']['build_statuses'] - /** @description Maximum number of items to return per page. */ - limit?: components['parameters']['builds_limit'] - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - cursor?: components['parameters']['builds_cursor'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned paginated builds. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildsListResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds/statuses': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get build statuses */ - get: { - parameters: { - query: { - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: components['parameters']['build_ids'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned build statuses */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildsStatusesResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/builds/{build_id}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get build details */ - get: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the build. */ - build_id: components['parameters']['build_id'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned build details. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['BuildInfo'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/record': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Get sandbox record */ - get: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the sandbox. */ - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned sandbox details. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxRecord'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * List user teams - * @description Returns all teams the authenticated user belongs to, with limits and default flag. - */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned user teams. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['UserTeamsResponse'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams/resolve': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Resolve team identity - * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. - */ - get: { - parameters: { - query: { - /** @description Team slug to resolve. */ - slug: components['parameters']['teamSlug'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully resolved team. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TeamResolveResponse'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams/{teamId}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post?: never - delete?: never - options?: never - head?: never - /** Update team */ - patch: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamId: components['parameters']['teamId'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['UpdateTeamRequest'] - } - } - responses: { - /** @description Successfully updated team. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['UpdateTeamResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - trace?: never - } - '/teams/{teamId}/members': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** List team members */ - get: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamId: components['parameters']['teamId'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned team members. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TeamMembersResponse'] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - /** Add team member */ - post: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamId: components['parameters']['teamId'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['AddTeamMemberRequest'] - } - } - responses: { - /** @description Successfully added team member. */ - 201: { - headers: { - [name: string]: unknown - } - content?: never - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams/{teamId}/members/{userId}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post?: never - /** Remove team member */ - delete: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamId: components['parameters']['teamId'] - /** @description Identifier of the user. */ - userId: components['parameters']['userId'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully removed team member. */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/defaults': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * List default templates - * @description Returns the list of default templates with their latest build info and aliases. - */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned default templates. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['DefaultTemplatesResponse'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } -} -export type webhooks = Record -export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code. - */ - code: number - /** @description Error message. */ - message: string - } - /** - * @description Build status mapped for dashboard clients. - * @enum {string} - */ - BuildStatus: 'building' | 'failed' | 'success' - ListedBuild: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string - /** @description Template alias when present, otherwise template ID. */ - template: string - /** @description Identifier of the template. */ - templateId: string - status: components['schemas']['BuildStatus'] - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - } - BuildsListResponse: { - data: components['schemas']['ListedBuild'][] - /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ - nextCursor: string | null - } - BuildStatusItem: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string - status: components['schemas']['BuildStatus'] - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - } - BuildsStatusesResponse: { - /** @description List of build statuses */ - buildStatuses: components['schemas']['BuildStatusItem'][] - } - BuildInfo: { - /** @description Template names related to this build, if available. */ - names?: string[] | null - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null - status: components['schemas']['BuildStatus'] - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null - } - /** - * Format: int64 - * @description CPU cores for the sandbox - */ - CPUCount: number - /** - * Format: int64 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number - /** - * Format: int64 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number - SandboxRecord: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string - /** @description Alias of the template */ - alias?: string - /** @description Identifier of the sandbox */ - sandboxID: string - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string - /** - * Format: date-time - * @description Time when the sandbox was stopped - */ - stoppedAt?: string | null - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - } - HealthResponse: { - /** @description Human-readable health check result. */ - message: string - } - UserTeamLimits: { - /** Format: int64 */ - maxLengthHours: number - /** Format: int32 */ - concurrentSandboxes: number - /** Format: int32 */ - concurrentTemplateBuilds: number - /** Format: int32 */ - maxVcpu: number - /** Format: int32 */ - maxRamMb: number - /** Format: int32 */ - diskMb: number - } - UserTeam: { - /** Format: uuid */ - id: string - name: string - slug: string - tier: string - email: string - isDefault: boolean - limits: components['schemas']['UserTeamLimits'] - } - UserTeamsResponse: { - teams: components['schemas']['UserTeam'][] - } - TeamMember: { - /** Format: uuid */ - id: string - email: string - isDefault: boolean - /** Format: uuid */ - addedBy?: string | null - /** Format: date-time */ - createdAt: string | null - } - TeamMembersResponse: { - members: components['schemas']['TeamMember'][] - } - UpdateTeamRequest: { - name: string - } - UpdateTeamResponse: { - /** Format: uuid */ - id: string - name: string - } - AddTeamMemberRequest: { - /** Format: email */ - email: string - } - DefaultTemplateAlias: { - alias: string - namespace?: string | null - } - DefaultTemplate: { - id: string - aliases: components['schemas']['DefaultTemplateAlias'][] - /** Format: uuid */ - buildId: string - /** Format: int64 */ - ramMb: number - /** Format: int64 */ - vcpu: number - /** Format: int64 */ - totalDiskSizeMb: number | null - envdVersion?: string | null - /** Format: date-time */ - createdAt: string - public: boolean - /** Format: int32 */ - buildCount: number - /** Format: int64 */ - spawnCount: number - } - DefaultTemplatesResponse: { - templates: components['schemas']['DefaultTemplate'][] - } - TeamResolveResponse: { - /** Format: uuid */ - id: string - slug: string - } - } - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - } - parameters: { - /** @description Identifier of the build. */ - build_id: string - /** @description Identifier of the sandbox. */ - sandboxID: string - /** @description Maximum number of items to return per page. */ - builds_limit: number - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - builds_cursor: string - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template: string - /** @description Comma-separated list of build statuses to include. */ - build_statuses: components['schemas']['BuildStatus'][] - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: string[] - /** @description Identifier of the team. */ - teamId: string - /** @description Identifier of the user. */ - userId: string - /** @description Team slug to resolve. */ - teamSlug: string - } - requestBodies: never - headers: never - pathItems: never -} -export type $defs = Record -export type operations = Record diff --git a/src/types/dashboard.types.ts b/src/types/dashboard.types.ts deleted file mode 100644 index 974496149..000000000 --- a/src/types/dashboard.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Database } from './database.types' - -export type ClientTeam = Database['public']['Tables']['teams']['Row'] & { - is_default?: boolean - // provides a transformed name for teams that are default ones and have "unchanged" default names - // e.g. "max.mustermann@gmail.com" -> "Max.mustermann's Team" - transformed_default_name?: string -} diff --git a/src/types/errors.ts b/src/types/errors.ts deleted file mode 100644 index 04070a293..000000000 --- a/src/types/errors.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Types - -export type E2BErrorCode = - | 'UNAUTHENTICATED' - | 'UNAUTHORIZED' - | 'INVALID_PARAMETERS' - | 'INTERNAL_SERVER_ERROR' - | 'API_ERROR' - | 'UNKNOWN' - | string - -export class E2BError extends Error { - public code: E2BErrorCode - - constructor(code: E2BErrorCode, message: string) { - super(message) - this.name = 'E2BError' - this.code = code - } -} - -// Errors - -export const UnauthenticatedError = () => - new E2BError('UNAUTHENTICATED', 'User not authenticated') - -export const UnauthorizedError = (message: string) => - new E2BError('UNAUTHORIZED', message) - -export const InvalidApiKeyError = (message: string) => - new E2BError('INVALID_API_KEY', message) - -export const InvalidParametersError = (message: string) => - new E2BError('INVALID_PARAMETERS', message) - -export const ApiError = (message: string) => new E2BError('API_ERROR', message) - -export const UnknownError = (message?: string) => - new E2BError( - 'UNKNOWN', - message ?? - 'An Unexpected Error Occurred, please try again. If the problem persists, please contact support.' - ) diff --git a/src/types/infra-api.types.ts b/src/types/infra-api.types.ts deleted file mode 100644 index fb56ffef2..000000000 --- a/src/types/infra-api.types.ts +++ /dev/null @@ -1,3222 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/health': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Health check */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Request was successful */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all teams */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all teams */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Team'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams/{teamID}/metrics': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get metrics for the team */ - get: { - parameters: { - query?: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number - end?: number - } - header?: never - path: { - teamID: components['parameters']['teamID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the team metrics */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TeamMetric'][] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/teams/{teamID}/metrics/max': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get the maximum metrics for the team in the given interval */ - get: { - parameters: { - query: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number - end?: number - /** @description Metric to retrieve the maximum value for */ - metric: 'concurrent_sandboxes' | 'sandbox_start_rate' - } - header?: never - path: { - teamID: components['parameters']['teamID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the team metrics */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['MaxTeamMetric'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 403: components['responses']['403'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all running sandboxes */ - get: { - parameters: { - query?: { - /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ - metadata?: string - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all running sandboxes */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['ListedSandbox'][] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - /** @description Create a sandbox from the template */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['NewSandbox'] - } - } - responses: { - /** @description The sandbox was created successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Sandbox'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v2/sandboxes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all sandboxes */ - get: { - parameters: { - query?: { - /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ - metadata?: string - /** @description Filter sandboxes by one or more states */ - state?: components['schemas']['SandboxState'][] - /** @description Cursor to start the list from */ - nextToken?: components['parameters']['paginationNextToken'] - /** @description Maximum number of items to return per page */ - limit?: components['parameters']['paginationLimit'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all running sandboxes */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['ListedSandbox'][] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/metrics': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List metrics for given sandboxes */ - get: { - parameters: { - query: { - /** @description Comma-separated list of sandbox IDs to get metrics for */ - sandbox_ids: string[] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all running sandboxes with metrics */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxesWithMetrics'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/logs': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * @deprecated - * @description Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. - */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - start?: number - /** @description Maximum number of logs that should be returned */ - limit?: number - } - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox logs */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxLogs'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v2/sandboxes/{sandboxID}/logs': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get sandbox logs */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - cursor?: number - /** @description Maximum number of logs that should be returned */ - limit?: number - /** @description Direction of the logs that should be returned */ - direction?: components['schemas']['LogsDirection'] - /** @description Minimum log level to return. Logs below this level are excluded */ - level?: components['schemas']['LogLevel'] - /** @description Case-sensitive substring match on log message content */ - search?: string - } - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox logs */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxLogsV2Response'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get a sandbox by id */ - get: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxDetail'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - /** @description Kill a sandbox */ - delete: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description The sandbox was killed successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/metrics': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get sandbox metrics */ - get: { - parameters: { - query?: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number - end?: number - } - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the sandbox metrics */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SandboxMetric'][] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/pause': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Pause the sandbox */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description The sandbox was paused successfully and can be resumed */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 409: components['responses']['409'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/resume': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** - * @deprecated - * @description Resume the sandbox - */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['ResumedSandbox'] - } - } - responses: { - /** @description The sandbox was resumed successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Sandbox'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 409: components['responses']['409'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/connect': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['ConnectSandbox'] - } - } - responses: { - /** @description The sandbox was already running */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Sandbox'] - } - } - /** @description The sandbox was resumed successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Sandbox'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/timeout': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Set the timeout for the sandbox. The sandbox will expire x seconds from the time of the request. Calling this method multiple times overwrites the TTL, each time using the current timestamp as the starting point to measure the timeout duration. */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: { - content: { - 'application/json': { - /** - * Format: int32 - * @description Timeout in seconds from the current time after which the sandbox should expire - */ - timeout: number - } - } - } - responses: { - /** @description Successfully set the sandbox timeout */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/refreshes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Refresh the sandbox extending its time to live */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody?: { - content: { - 'application/json': { - /** @description Duration for which the sandbox should be kept alive in seconds */ - duration?: number - } - } - } - responses: { - /** @description Successfully refreshed the sandbox */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/sandboxes/{sandboxID}/snapshots': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. */ - post: { - parameters: { - query?: never - header?: never - path: { - sandboxID: components['parameters']['sandboxID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': { - /** @description Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. */ - name?: string - } - } - } - responses: { - /** @description Snapshot created successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SnapshotInfo'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/snapshots': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all snapshots for the team */ - get: { - parameters: { - query?: { - sandboxID?: string - /** @description Maximum number of items to return per page */ - limit?: components['parameters']['paginationLimit'] - /** @description Cursor to start the list from */ - nextToken?: components['parameters']['paginationNextToken'] - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned snapshots */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SnapshotInfo'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v3/templates': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Create a new template */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateBuildRequestV3'] - } - } - responses: { - /** @description The build was requested successfully */ - 202: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateRequestResponseV3'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v2/templates': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** - * @deprecated - * @description Create a new template - */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateBuildRequestV2'] - } - } - responses: { - /** @description The build was requested successfully */ - 202: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateLegacy'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/{templateID}/files/{hash}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get an upload link for a tar file containing build layer files */ - get: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - hash: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description The upload link where to upload the tar file */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateBuildFileUpload'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all templates */ - get: { - parameters: { - query?: { - teamID?: string - } - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all templates */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Template'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - /** - * @deprecated - * @description Create a new template - */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateBuildRequest'] - } - } - responses: { - /** @description The build was accepted */ - 202: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateLegacy'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/{templateID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all builds for a template */ - get: { - parameters: { - query?: { - /** @description Cursor to start the list from */ - nextToken?: components['parameters']['paginationNextToken'] - /** @description Maximum number of items to return per page */ - limit?: components['parameters']['paginationLimit'] - } - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the template with its builds */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateWithBuilds'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - /** - * @deprecated - * @description Rebuild an template - */ - post: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateBuildRequest'] - } - } - responses: { - /** @description The build was accepted */ - 202: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateLegacy'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - /** @description Delete a template */ - delete: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description The template was deleted successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - /** - * @deprecated - * @description Update template - */ - patch: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateUpdateRequest'] - } - } - responses: { - /** @description The template was updated successfully */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - trace?: never - } - '/templates/{templateID}/builds/{buildID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** - * @deprecated - * @description Start the build - */ - post: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - buildID: components['parameters']['buildID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description The build has started */ - 202: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v2/templates/{templateID}/builds/{buildID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Start the build */ - post: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - buildID: components['parameters']['buildID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateBuildStartV2'] - } - } - responses: { - /** @description The build has started */ - 202: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/v2/templates/{templateID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post?: never - delete?: never - options?: never - head?: never - /** @description Update template */ - patch: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['TemplateUpdateRequest'] - } - } - responses: { - /** @description The template was updated successfully */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateUpdateResponse'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - trace?: never - } - '/templates/{templateID}/builds/{buildID}/status': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get template build info */ - get: { - parameters: { - query?: { - /** @description Index of the starting build log that should be returned with the template */ - logsOffset?: number - /** @description Maximum number of logs that should be returned */ - limit?: number - level?: components['schemas']['LogLevel'] - } - header?: never - path: { - templateID: components['parameters']['templateID'] - buildID: components['parameters']['buildID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the template */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateBuildInfo'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/{templateID}/builds/{buildID}/logs': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get template build logs */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - cursor?: number - /** @description Maximum number of logs that should be returned */ - limit?: number - direction?: components['schemas']['LogsDirection'] - level?: components['schemas']['LogLevel'] - /** @description Source of the logs that should be returned from */ - source?: components['schemas']['LogsSource'] - } - header?: never - path: { - templateID: components['parameters']['templateID'] - buildID: components['parameters']['buildID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the template build logs */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateBuildLogsResponse'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/tags': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Assign tag(s) to a template build */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['AssignTemplateTagsRequest'] - } - } - responses: { - /** @description Tag assigned successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['AssignedTemplateTags'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - /** @description Delete multiple tags from templates */ - delete: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['DeleteTemplateTagsRequest'] - } - } - responses: { - /** @description Tags deleted successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/{templateID}/tags': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all tags for a template */ - get: { - parameters: { - query?: never - header?: never - path: { - templateID: components['parameters']['templateID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the template tags */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateTag'][] - } - } - 401: components['responses']['401'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/templates/aliases/{alias}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Check if template with given alias exists */ - get: { - parameters: { - query?: never - header?: never - path: { - alias: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully queried template by alias */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TemplateAliasResponse'] - } - } - 400: components['responses']['400'] - 403: components['responses']['403'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/nodes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all nodes */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all nodes */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Node'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/nodes/{nodeID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get node info */ - get: { - parameters: { - query?: { - /** @description Identifier of the cluster */ - clusterID?: string - } - header?: never - path: { - nodeID: components['parameters']['nodeID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned the node */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['NodeDetail'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - /** @description Change status of a node */ - post: { - parameters: { - query?: never - header?: never - path: { - nodeID: components['parameters']['nodeID'] - } - cookie?: never - } - requestBody?: { - content: { - 'application/json': components['schemas']['NodeStatusChange'] - } - } - responses: { - /** @description The node status was changed successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/admin/teams/{teamID}/sandboxes/kill': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** - * Kill all sandboxes for a team - * @description Kills all sandboxes for the specified team - */ - post: { - parameters: { - query?: never - header?: never - path: { - /** @description Team ID */ - teamID: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully killed sandboxes */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['AdminSandboxKillResult'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/admin/teams/{teamID}/builds/cancel': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** - * Cancel all builds for a team - * @description Cancels all in-progress and pending builds for the specified team - */ - post: { - parameters: { - query?: never - header?: never - path: { - /** @description Team ID */ - teamID: string - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully cancelled builds */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['AdminBuildCancelResult'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/access-tokens': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - /** @description Create a new access token */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['NewAccessToken'] - } - } - responses: { - /** @description Access token created successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['CreatedAccessToken'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/access-tokens/{accessTokenID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post?: never - /** @description Delete an access token */ - delete: { - parameters: { - query?: never - header?: never - path: { - accessTokenID: components['parameters']['accessTokenID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Access token deleted successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - patch?: never - trace?: never - } - '/api-keys': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all team API keys */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully returned all team API keys */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['TeamAPIKey'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - /** @description Create a new team API key */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['NewTeamAPIKey'] - } - } - responses: { - /** @description Team API key created successfully */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['CreatedTeamAPIKey'] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/api-keys/{apiKeyID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - get?: never - put?: never - post?: never - /** @description Delete a team API key */ - delete: { - parameters: { - query?: never - header?: never - path: { - apiKeyID: components['parameters']['apiKeyID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Team API key deleted successfully */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - /** @description Update a team API key */ - patch: { - parameters: { - query?: never - header?: never - path: { - apiKeyID: components['parameters']['apiKeyID'] - } - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['UpdateTeamAPIKey'] - } - } - responses: { - /** @description Team API key updated successfully */ - 200: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - trace?: never - } - '/volumes': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description List all team volumes */ - get: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully listed all team volumes */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Volume'][] - } - } - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - put?: never - /** @description Create a new team volume */ - post: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody: { - content: { - 'application/json': components['schemas']['NewVolume'] - } - } - responses: { - /** @description Successfully created a new team volume */ - 201: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['VolumeAndToken'] - } - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - } - } - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/volumes/{volumeID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get team volume info */ - get: { - parameters: { - query?: never - header?: never - path: { - volumeID: components['parameters']['volumeID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully retrieved a team volume */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['VolumeAndToken'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - put?: never - post?: never - /** @description Delete a team volume */ - delete: { - parameters: { - query?: never - header?: never - path: { - volumeID: components['parameters']['volumeID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Successfully deleted a team volume */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } - options?: never - head?: never - patch?: never - trace?: never - } -} -export type webhooks = Record -export interface components { - schemas: { - Team: { - /** @description Identifier of the team */ - teamID: string - /** @description Name of the team */ - name: string - /** @description API key for the team */ - apiKey: string - /** @description Whether the team is the default team */ - isDefault: boolean - } - TeamUser: { - /** - * Format: uuid - * @description Identifier of the user - */ - id: string - /** @description Email of the user */ - email: string - } - TemplateUpdateRequest: { - /** @description Whether the template is public or only accessible by the team */ - public?: boolean - } - TemplateUpdateResponse: { - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[] - } - /** - * Format: int32 - * @description CPU cores for the sandbox - */ - CPUCount: number - /** - * Format: int32 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number - /** - * Format: int32 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number - /** @description Version of the envd running in the sandbox */ - EnvdVersion: string - SandboxMetadata: { - [key: string]: string - } - /** - * @description State of the sandbox - * @enum {string} - */ - SandboxState: 'running' | 'paused' - SnapshotInfo: { - /** @description Identifier of the snapshot template including the tag. Uses namespace/alias when a name was provided (e.g. team-slug/my-snapshot:default), otherwise falls back to the raw template ID (e.g. abc123:default). */ - snapshotID: string - /** @description Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) */ - names: string[] - } - EnvVars: { - [key: string]: string - } - /** @description MCP configuration for the sandbox */ - Mcp: { - [key: string]: unknown - } | null - SandboxNetworkConfig: { - /** - * @description Specify if the sandbox URLs should be accessible only with authentication. - * @default true - */ - allowPublicTraffic: boolean - /** @description List of allowed CIDR blocks or IP addresses for egress traffic. Allowed addresses always take precedence over blocked addresses. */ - allowOut?: string[] - /** @description List of denied CIDR blocks or IP addresses for egress traffic */ - denyOut?: string[] - /** @description Specify host mask which will be used for all sandbox requests */ - maskRequestHost?: string - } - /** - * @description Auto-resume enabled flag for paused sandboxes. Default false. - * @default false - */ - SandboxAutoResumeEnabled: boolean - /** @description Auto-resume configuration for paused sandboxes. */ - SandboxAutoResumeConfig: { - enabled: components['schemas']['SandboxAutoResumeEnabled'] - } - /** @description Log entry with timestamp and line */ - SandboxLog: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string - /** @description Log line content */ - line: string - } - SandboxLogEntry: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string - /** @description Log message content */ - message: string - level: components['schemas']['LogLevel'] - fields: { - [key: string]: string - } - } - SandboxLogs: { - /** @description Logs of the sandbox */ - logs: components['schemas']['SandboxLog'][] - /** @description Structured logs of the sandbox */ - logEntries: components['schemas']['SandboxLogEntry'][] - } - SandboxLogsV2Response: { - /** - * @description Sandbox logs structured - * @default [] - */ - logs: components['schemas']['SandboxLogEntry'][] - } - /** @description Metric entry with timestamp and line */ - SandboxMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number - /** - * Format: int32 - * @description Number of CPU cores - */ - cpuCount: number - /** - * Format: float - * @description CPU usage percentage - */ - cpuUsedPct: number - /** - * Format: int64 - * @description Memory used in bytes - */ - memUsed: number - /** - * Format: int64 - * @description Total memory in bytes - */ - memTotal: number - /** - * Format: int64 - * @description Disk used in bytes - */ - diskUsed: number - /** - * Format: int64 - * @description Total disk space in bytes - */ - diskTotal: number - } - SandboxVolumeMount: { - /** @description Name of the volume */ - name: string - /** @description Path of the volume */ - path: string - } - Sandbox: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string - /** @description Identifier of the sandbox */ - sandboxID: string - /** @description Alias of the template */ - alias?: string - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string - envdVersion: components['schemas']['EnvdVersion'] - /** @description Access token used for envd communication */ - envdAccessToken?: string - /** @description Token required for accessing sandbox via proxy. */ - trafficAccessToken?: string | null - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null - } - SandboxDetail: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string - /** @description Alias of the template */ - alias?: string - /** @description Identifier of the sandbox */ - sandboxID: string - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string - /** - * Format: date-time - * @description Time when the sandbox will expire - */ - endAt: string - envdVersion: components['schemas']['EnvdVersion'] - /** @description Access token used for envd communication */ - envdAccessToken?: string - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - metadata?: components['schemas']['SandboxMetadata'] - state: components['schemas']['SandboxState'] - volumeMounts?: components['schemas']['SandboxVolumeMount'][] - } - ListedSandbox: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string - /** @description Alias of the template */ - alias?: string - /** @description Identifier of the sandbox */ - sandboxID: string - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string - /** - * Format: date-time - * @description Time when the sandbox will expire - */ - endAt: string - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - metadata?: components['schemas']['SandboxMetadata'] - state: components['schemas']['SandboxState'] - envdVersion: components['schemas']['EnvdVersion'] - volumeMounts?: components['schemas']['SandboxVolumeMount'][] - } - SandboxesWithMetrics: { - sandboxes: { - [key: string]: components['schemas']['SandboxMetric'] - } - } - NewSandbox: { - /** @description Identifier of the required template */ - templateID: string - /** - * Format: int32 - * @description Time to live for the sandbox in seconds. - * @default 15 - */ - timeout: number - /** - * @description Automatically pauses the sandbox after the timeout - * @default false - */ - autoPause: boolean - autoResume?: components['schemas']['SandboxAutoResumeConfig'] - /** @description Secure all system communication with sandbox */ - secure?: boolean - /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ - allow_internet_access?: boolean - network?: components['schemas']['SandboxNetworkConfig'] - metadata?: components['schemas']['SandboxMetadata'] - envVars?: components['schemas']['EnvVars'] - mcp?: components['schemas']['Mcp'] - volumeMounts?: components['schemas']['SandboxVolumeMount'][] - } - ResumedSandbox: { - /** - * Format: int32 - * @description Time to live for the sandbox in seconds. - * @default 15 - */ - timeout: number - /** - * @deprecated - * @description Automatically pauses the sandbox after the timeout - */ - autoPause?: boolean - } - ConnectSandbox: { - /** - * Format: int32 - * @description Timeout in seconds from the current time after which the sandbox should expire - */ - timeout: number - } - /** @description Team metric with timestamp */ - TeamMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number - /** - * Format: int32 - * @description The number of concurrent sandboxes for the team - */ - concurrentSandboxes: number - /** - * Format: float - * @description Number of sandboxes started per second - */ - sandboxStartRate: number - } - /** @description Team metric with timestamp */ - MaxTeamMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number - /** @description The maximum value of the requested metric in the given interval */ - value: number - } - AdminSandboxKillResult: { - /** @description Number of sandboxes successfully killed */ - killedCount: number - /** @description Number of sandboxes that failed to kill */ - failedCount: number - } - AdminBuildCancelResult: { - /** @description Number of builds successfully cancelled */ - cancelledCount: number - /** @description Number of builds that failed to cancel */ - failedCount: number - } - VolumeToken: { - token: string - } - Template: { - /** @description Identifier of the template */ - templateID: string - /** @description Identifier of the last successful build for given template */ - buildID: string - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - /** @description Whether the template is public or only accessible by the team */ - public: boolean - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[] - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[] - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string - createdBy: components['schemas']['TeamUser'] | null - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number - /** - * Format: int32 - * @description Number of times the template was built - */ - buildCount: number - envdVersion: components['schemas']['EnvdVersion'] - buildStatus: components['schemas']['TemplateBuildStatus'] - } - TemplateRequestResponseV3: { - /** @description Identifier of the template */ - templateID: string - /** @description Identifier of the last successful build for given template */ - buildID: string - /** @description Whether the template is public or only accessible by the team */ - public: boolean - /** @description Names of the template */ - names: string[] - /** @description Tags assigned to the template build */ - tags: string[] - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[] - } - TemplateLegacy: { - /** @description Identifier of the template */ - templateID: string - /** @description Identifier of the last successful build for given template */ - buildID: string - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB: components['schemas']['DiskSizeMB'] - /** @description Whether the template is public or only accessible by the team */ - public: boolean - /** @description Aliases of the template */ - aliases: string[] - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string - createdBy: components['schemas']['TeamUser'] | null - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number - /** - * Format: int32 - * @description Number of times the template was built - */ - buildCount: number - envdVersion: components['schemas']['EnvdVersion'] - } - TemplateBuild: { - /** - * Format: uuid - * @description Identifier of the build - */ - buildID: string - status: components['schemas']['TemplateBuildStatus'] - /** - * Format: date-time - * @description Time when the build was created - */ - createdAt: string - /** - * Format: date-time - * @description Time when the build was last updated - */ - updatedAt: string - /** - * Format: date-time - * @description Time when the build was finished - */ - finishedAt?: string - cpuCount: components['schemas']['CPUCount'] - memoryMB: components['schemas']['MemoryMB'] - diskSizeMB?: components['schemas']['DiskSizeMB'] - envdVersion?: components['schemas']['EnvdVersion'] - } - TemplateWithBuilds: { - /** @description Identifier of the template */ - templateID: string - /** @description Whether the template is public or only accessible by the team */ - public: boolean - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[] - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[] - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number - /** @description List of builds for the template */ - builds: components['schemas']['TemplateBuild'][] - } - TemplateAliasResponse: { - /** @description Identifier of the template */ - templateID: string - /** @description Whether the template is public or only accessible by the team */ - public: boolean - } - TemplateBuildRequest: { - /** @description Alias of the template */ - alias?: string - /** @description Dockerfile for the template */ - dockerfile: string - /** @description Identifier of the team */ - teamID?: string - /** @description Start command to execute in the template after the build */ - startCmd?: string - /** @description Ready check command to execute in the template after the build */ - readyCmd?: string - cpuCount?: components['schemas']['CPUCount'] - memoryMB?: components['schemas']['MemoryMB'] - } - /** @description Step in the template build process */ - TemplateStep: { - /** @description Type of the step */ - type: string - /** - * @description Arguments for the step - * @default [] - */ - args: string[] - /** @description Hash of the files used in the step */ - filesHash?: string - /** - * @description Whether the step should be forced to run regardless of the cache - * @default false - */ - force: boolean - } - TemplateBuildRequestV3: { - /** @description Name of the template. Can include a tag with colon separator (e.g. "my-template" or "my-template:v1"). If tag is included, it will be treated as if the tag was provided in the tags array. */ - name?: string - /** @description Tags to assign to the template build */ - tags?: string[] - /** - * @deprecated - * @description Alias of the template. Deprecated, use name instead. - */ - alias?: string - /** - * @deprecated - * @description Identifier of the team - */ - teamID?: string - cpuCount?: components['schemas']['CPUCount'] - memoryMB?: components['schemas']['MemoryMB'] - } - TemplateBuildRequestV2: { - /** @description Alias of the template */ - alias: string - /** - * @deprecated - * @description Identifier of the team - */ - teamID?: string - cpuCount?: components['schemas']['CPUCount'] - memoryMB?: components['schemas']['MemoryMB'] - } - FromImageRegistry: - | components['schemas']['AWSRegistry'] - | components['schemas']['GCPRegistry'] - | components['schemas']['GeneralRegistry'] - AWSRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: 'aws' - /** @description AWS Access Key ID for ECR authentication */ - awsAccessKeyId: string - /** @description AWS Secret Access Key for ECR authentication */ - awsSecretAccessKey: string - /** @description AWS Region where the ECR registry is located */ - awsRegion: string - } - GCPRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: 'gcp' - /** @description Service Account JSON for GCP authentication */ - serviceAccountJson: string - } - GeneralRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: 'registry' - /** @description Username to use for the registry */ - username: string - /** @description Password to use for the registry */ - password: string - } - TemplateBuildStartV2: { - /** @description Image to use as a base for the template build */ - fromImage?: string - /** @description Template to use as a base for the template build */ - fromTemplate?: string - fromImageRegistry?: components['schemas']['FromImageRegistry'] - /** - * @description Whether the whole build should be forced to run regardless of the cache - * @default false - */ - force: boolean - /** - * @description List of steps to execute in the template build - * @default [] - */ - steps: components['schemas']['TemplateStep'][] - /** @description Start command to execute in the template after the build */ - startCmd?: string - /** @description Ready check command to execute in the template after the build */ - readyCmd?: string - } - TemplateBuildFileUpload: { - /** @description Whether the file is already present in the cache */ - present: boolean - /** @description Url where the file should be uploaded to */ - url?: string - } - /** - * @description State of the sandbox - * @enum {string} - */ - LogLevel: 'debug' | 'info' | 'warn' | 'error' - BuildLogEntry: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string - /** @description Log message content */ - message: string - level: components['schemas']['LogLevel'] - /** @description Step in the build process related to the log entry */ - step?: string - } - BuildStatusReason: { - /** @description Message with the status reason, currently reporting only for error status */ - message: string - /** @description Step that failed */ - step?: string - /** - * @description Log entries related to the status reason - * @default [] - */ - logEntries: components['schemas']['BuildLogEntry'][] - } - /** - * @description Status of the template build - * @enum {string} - */ - TemplateBuildStatus: 'building' | 'waiting' | 'ready' | 'error' - TemplateBuildInfo: { - /** - * @description Build logs - * @default [] - */ - logs: string[] - /** - * @description Build logs structured - * @default [] - */ - logEntries: components['schemas']['BuildLogEntry'][] - /** @description Identifier of the template */ - templateID: string - /** @description Identifier of the build */ - buildID: string - status: components['schemas']['TemplateBuildStatus'] - reason?: components['schemas']['BuildStatusReason'] - } - TemplateBuildLogsResponse: { - /** - * @description Build logs structured - * @default [] - */ - logs: components['schemas']['BuildLogEntry'][] - } - /** - * @description Direction of the logs that should be returned - * @enum {string} - */ - LogsDirection: 'forward' | 'backward' - /** - * @description Source of the logs that should be returned - * @enum {string} - */ - LogsSource: 'temporary' | 'persistent' - /** - * @description Status of the node - * @enum {string} - */ - NodeStatus: 'ready' | 'draining' | 'connecting' | 'unhealthy' - NodeStatusChange: { - /** - * Format: uuid - * @description Identifier of the cluster - */ - clusterID?: string - status: components['schemas']['NodeStatus'] - } - DiskMetrics: { - /** @description Mount point of the disk */ - mountPoint: string - /** @description Device name */ - device: string - /** @description Filesystem type (e.g., ext4, xfs) */ - filesystemType: string - /** - * Format: uint64 - * @description Used space in bytes - */ - usedBytes: number - /** - * Format: uint64 - * @description Total space in bytes - */ - totalBytes: number - } - /** @description Node metrics */ - NodeMetrics: { - /** - * Format: uint32 - * @description Number of allocated CPU cores - */ - allocatedCPU: number - /** - * Format: uint32 - * @description Node CPU usage percentage - */ - cpuPercent: number - /** - * Format: uint32 - * @description Total number of CPU cores on the node - */ - cpuCount: number - /** - * Format: uint64 - * @description Amount of allocated memory in bytes - */ - allocatedMemoryBytes: number - /** - * Format: uint64 - * @description Node memory used in bytes - */ - memoryUsedBytes: number - /** - * Format: uint64 - * @description Total node memory in bytes - */ - memoryTotalBytes: number - /** @description Detailed metrics for each disk/mount point */ - disks: components['schemas']['DiskMetrics'][] - } - MachineInfo: { - /** @description CPU family of the node */ - cpuFamily: string - /** @description CPU model of the node */ - cpuModel: string - /** @description CPU model name of the node */ - cpuModelName: string - /** @description CPU architecture of the node */ - cpuArchitecture: string - } - Node: { - /** @description Version of the orchestrator */ - version: string - /** @description Commit of the orchestrator */ - commit: string - /** @description Identifier of the node */ - id: string - /** @description Service instance identifier of the node */ - serviceInstanceID: string - /** @description Identifier of the cluster */ - clusterID: string - machineInfo: components['schemas']['MachineInfo'] - status: components['schemas']['NodeStatus'] - /** - * Format: uint32 - * @description Number of sandboxes running on the node - */ - sandboxCount: number - metrics: components['schemas']['NodeMetrics'] - /** - * Format: uint64 - * @description Number of sandbox create successes - */ - createSuccesses: number - /** - * Format: uint64 - * @description Number of sandbox create fails - */ - createFails: number - /** - * Format: int - * @description Number of starting Sandboxes - */ - sandboxStartingCount: number - } - NodeDetail: { - /** @description Identifier of the cluster */ - clusterID: string - /** @description Version of the orchestrator */ - version: string - /** @description Commit of the orchestrator */ - commit: string - /** @description Identifier of the node */ - id: string - /** @description Service instance identifier of the node */ - serviceInstanceID: string - machineInfo: components['schemas']['MachineInfo'] - status: components['schemas']['NodeStatus'] - /** - * Format: uint32 - * @description Number of sandboxes running on the node - */ - sandboxCount: number - metrics: components['schemas']['NodeMetrics'] - /** @description List of cached builds id on the node */ - cachedBuilds: string[] - /** - * Format: uint64 - * @description Number of sandbox create successes - */ - createSuccesses: number - /** - * Format: uint64 - * @description Number of sandbox create fails - */ - createFails: number - } - CreatedAccessToken: { - /** - * Format: uuid - * @description Identifier of the access token - */ - id: string - /** @description Name of the access token */ - name: string - /** @description The fully created access token */ - token: string - mask: components['schemas']['IdentifierMaskingDetails'] - /** - * Format: date-time - * @description Timestamp of access token creation - */ - createdAt: string - } - NewAccessToken: { - /** @description Name of the access token */ - name: string - } - TeamAPIKey: { - /** - * Format: uuid - * @description Identifier of the API key - */ - id: string - /** @description Name of the API key */ - name: string - mask: components['schemas']['IdentifierMaskingDetails'] - /** - * Format: date-time - * @description Timestamp of API key creation - */ - createdAt: string - createdBy?: components['schemas']['TeamUser'] | null - /** - * Format: date-time - * @description Last time this API key was used - */ - lastUsed?: string | null - } - CreatedTeamAPIKey: { - /** - * Format: uuid - * @description Identifier of the API key - */ - id: string - /** @description Raw value of the API key */ - key: string - mask: components['schemas']['IdentifierMaskingDetails'] - /** @description Name of the API key */ - name: string - /** - * Format: date-time - * @description Timestamp of API key creation - */ - createdAt: string - createdBy?: components['schemas']['TeamUser'] | null - /** - * Format: date-time - * @description Last time this API key was used - */ - lastUsed?: string | null - } - NewTeamAPIKey: { - /** @description Name of the API key */ - name: string - } - UpdateTeamAPIKey: { - /** @description New name for the API key */ - name: string - } - AssignedTemplateTags: { - /** @description Assigned tags of the template */ - tags: string[] - /** - * Format: uuid - * @description Identifier of the build associated with these tags - */ - buildID: string - } - TemplateTag: { - /** @description The tag name */ - tag: string - /** - * Format: uuid - * @description Identifier of the build associated with this tag - */ - buildID: string - /** - * Format: date-time - * @description Time when the tag was assigned - */ - createdAt: string - } - AssignTemplateTagsRequest: { - /** @description Target template in "name:tag" format */ - target: string - /** @description Tags to assign to the template */ - tags: string[] - } - DeleteTemplateTagsRequest: { - /** @description Name of the template */ - name: string - /** @description Tags to delete */ - tags: string[] - } - Error: { - /** - * Format: int32 - * @description Error code - */ - code: number - /** @description Error */ - message: string - } - IdentifierMaskingDetails: { - /** @description Prefix that identifies the token or key type */ - prefix: string - /** @description Length of the token or key */ - valueLength: number - /** @description Prefix used in masked version of the token or key */ - maskedValuePrefix: string - /** @description Suffix used in masked version of the token or key */ - maskedValueSuffix: string - } - Volume: { - /** @description ID of the volume */ - volumeID: string - /** @description Name of the volume */ - name: string - } - VolumeAndToken: { - /** @description ID of the volume */ - volumeID: string - /** @description Name of the volume */ - name: string - /** @description Auth token to use for interacting with volume content */ - token: string - } - NewVolume: { - /** @description Name of the volume */ - name: string - } - } - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['Error'] - } - } - } - parameters: { - templateID: string - buildID: string - sandboxID: string - teamID: string - nodeID: string - apiKeyID: string - accessTokenID: string - snapshotID: string - tag: string - /** @description Maximum number of items to return per page */ - paginationLimit: number - /** @description Cursor to start the list from */ - paginationNextToken: string - volumeID: string - } - requestBodies: never - headers: never - pathItems: never -} -export type $defs = Record -export type operations = Record diff --git a/tsconfig.json b/tsconfig.json index 80b35f972..40c9f36a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,17 @@ "paths": { "@/domains/*": ["./src/core/domains/*"], "@/shared/*": ["./src/core/shared/*"], + "@/lib/clients/action": ["./src/core/server/actions/client.ts"], + "@/lib/clients/*": ["./src/core/shared/clients/*"], + "@/lib/utils/action": ["./src/core/server/actions/utils.ts"], + "@/lib/schemas/*": ["./src/core/shared/schemas/*"], + "@/types/argus-api.types": ["./src/core/shared/contracts/argus-api.types.ts"], + "@/types/dashboard-api.types": [ + "./src/core/shared/contracts/dashboard-api.types.ts" + ], + "@/types/infra-api.types": ["./src/core/shared/contracts/infra-api.types.ts"], + "@/types/database.types": ["./src/core/shared/contracts/database.types.ts"], + "@/types/errors": ["./src/core/shared/errors.ts"], "@/server/api/errors": ["./src/core/server/adapters/trpc-errors.ts"], "@/server/api/models/builds.models": [ "./src/core/domains/builds/models.ts" From 38915466392ac24159fd24783cd54a98d1663c26 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 18 Mar 2026 13:00:54 -0700 Subject: [PATCH 05/37] refactor: explicitly scoped repo factories and repo scope inference through safe-action and trpc middlewares --- src/app/(auth)/auth/cli/page.tsx | 4 +- src/app/api/auth/verify-otp/route.ts | 6 +- src/app/api/teams/user/route.ts | 7 +- .../inspect/sandbox/[sandboxId]/route.ts | 7 +- src/app/sbx/new/route.ts | 7 +- src/core/domains/auth/repository.server.ts | 35 +-- src/core/domains/billing/repository.server.ts | 6 +- src/core/domains/builds/repository.server.ts | 153 +++++++----- src/core/domains/keys/repository.server.ts | 6 +- .../domains/sandboxes/repository.server.ts | 220 +++++++++++------- src/core/domains/support/repository.server.ts | 73 +++--- ...y.server.ts => teams-repository.server.ts} | 161 ++----------- .../teams/user-teams-repository.server.ts | 147 ++++++++++++ .../domains/templates/repository.server.ts | 149 ++++++------ .../domains/webhooks/repository.server.ts | 6 +- src/core/server/actions/client.ts | 79 +++++-- src/core/server/actions/key-actions.ts | 20 +- src/core/server/actions/team-actions.ts | 26 ++- src/core/server/actions/webhooks-actions.ts | 21 +- src/core/server/api/middlewares/auth.ts | 4 - src/core/server/api/middlewares/repository.ts | 54 +++++ src/core/server/api/routers/billing.ts | 65 +++--- src/core/server/api/routers/builds.ts | 61 +++-- src/core/server/api/routers/sandbox.ts | 62 +++-- src/core/server/api/routers/sandboxes.ts | 47 +++- src/core/server/api/routers/support.ts | 29 ++- src/core/server/api/routers/teams.ts | 12 +- src/core/server/api/routers/templates.ts | 42 +++- src/core/server/context/from-route.ts | 11 - src/core/server/context/request-context.ts | 68 ------ .../server/functions/keys/get-api-keys.ts | 13 +- .../team/get-team-id-from-segment.ts | 4 +- .../server/functions/team/get-team-limits.ts | 17 +- .../server/functions/team/get-team-members.ts | 15 +- src/core/server/functions/team/get-team.ts | 18 +- .../functions/team/resolve-user-team.ts | 4 +- src/core/server/functions/usage/get-usage.ts | 12 +- .../server/functions/webhooks/get-webhooks.ts | 15 +- src/core/server/trpc/init.ts | 2 - src/core/server/trpc/procedures.ts | 5 - src/core/shared/repository-scope.ts | 7 + 41 files changed, 1045 insertions(+), 655 deletions(-) rename src/core/domains/teams/{repository.server.ts => teams-repository.server.ts} (55%) create mode 100644 src/core/domains/teams/user-teams-repository.server.ts create mode 100644 src/core/server/api/middlewares/repository.ts delete mode 100644 src/core/server/context/from-route.ts delete mode 100644 src/core/server/context/request-context.ts create mode 100644 src/core/shared/repository-scope.ts diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 0ffb86191..87e007846 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation' import { Suspense } from 'react' import { serializeError } from 'serialize-error' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createTeamsRepository } from '@/domains/teams/repository.server' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' @@ -28,7 +28,7 @@ async function handleCLIAuth( throw new Error('Invalid redirect URL') } - const teamsResult = await createTeamsRepository({ + const teamsResult = await createUserTeamsRepository({ accessToken: supabaseAccessToken, }).listUserTeams() diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 521ba79aa..25d66fd5a 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -109,7 +109,11 @@ export async function POST(request: NextRequest) { `verifying OTP token: ${token_hash.slice(0, 10)}` ) - await authRepository.verifyOtp(token_hash, type) + const verifyResult = await authRepository.verifyOtp(token_hash, type) + if (!verifyResult.ok) { + const errorRedirectUrl = buildErrorRedirectUrl(origin, verifyResult.error.message) + return NextResponse.json({ redirectUrl: errorRedirectUrl }) + } const redirectUrl = buildRedirectUrl(type, next, origin) diff --git a/src/app/api/teams/user/route.ts b/src/app/api/teams/user/route.ts index ea10a0fac..83164e8ad 100644 --- a/src/app/api/teams/user/route.ts +++ b/src/app/api/teams/user/route.ts @@ -1,4 +1,4 @@ -import { createRouteServices } from '@/core/server/context/from-route' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { createClient } from '@/lib/clients/supabase/server' import type { UserTeamsResponse } from './types' @@ -12,8 +12,9 @@ export async function GET() { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - const services = createRouteServices({ accessToken: session.access_token }) - const teamsResult = await services.teams.listUserTeams() + const teamsResult = await createUserTeamsRepository({ + accessToken: session.access_token, + }).listUserTeams() if (!teamsResult.ok) { return Response.json({ error: 'Failed to fetch teams' }, { status: 500 }) diff --git a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts index 223d83b3c..a0ded9274 100644 --- a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts +++ b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts @@ -4,7 +4,7 @@ import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createRouteServices } from '@/core/server/context/from-route' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' @@ -159,8 +159,9 @@ export async function GET( } const accessToken = sessionResponse.session.access_token - const services = createRouteServices({ accessToken }) - const teamsResult = await services.teams.listUserTeams() + const teamsResult = await createUserTeamsRepository({ + accessToken, + }).listUserTeams() if (!teamsResult.ok || teamsResult.data.length === 0) { l.warn({ diff --git a/src/app/sbx/new/route.ts b/src/app/sbx/new/route.ts index 470188c69..bd93bb2b1 100644 --- a/src/app/sbx/new/route.ts +++ b/src/app/sbx/new/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createRouteServices } from '@/core/server/context/from-route' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' @@ -35,8 +35,9 @@ export const GET = async (req: NextRequest) => { ) } - const services = createRouteServices({ accessToken: session.access_token }) - const teamsResult = await services.teams.listUserTeams() + const teamsResult = await createUserTeamsRepository({ + accessToken: session.access_token, + }).listUserTeams() const defaultTeam = teamsResult.ok ? (teamsResult.data.find((team) => team.is_default) ?? teamsResult.data[0]) diff --git a/src/core/domains/auth/repository.server.ts b/src/core/domains/auth/repository.server.ts index 3fd4bd0fa..9f59b9289 100644 --- a/src/core/domains/auth/repository.server.ts +++ b/src/core/domains/auth/repository.server.ts @@ -1,8 +1,9 @@ import 'server-only' -import { TRPCError } from '@trpc/server' import { serializeError } from 'serialize-error' import type { OtpType } from '@/core/domains/auth/models' +import { repoErrorFromHttp } from '@/core/shared/errors' +import { err, ok, type RepoResult } from '@/core/shared/result' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' @@ -15,7 +16,10 @@ type AuthRepositoryDeps = { } export interface AuthRepository { - verifyOtp(tokenHash: string, type: OtpType): Promise + verifyOtp( + tokenHash: string, + type: OtpType + ): Promise> } export function createAuthRepository(deps: AuthRepositoryDeps): AuthRepository { @@ -44,28 +48,27 @@ export function createAuthRepository(deps: AuthRepositoryDeps): AuthRepository { ) if (error.status === 403 && error.code === 'otp_expired') { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Email link has expired. Please request a new one.', - }) + return err( + repoErrorFromHttp( + 400, + 'Email link has expired. Please request a new one.', + error + ) + ) } - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Invalid or expired verification link.', - }) + return err( + repoErrorFromHttp(400, 'Invalid or expired verification link.', error) + ) } if (!data.user) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Verification failed. Please try again.', - }) + return err(repoErrorFromHttp(500, 'Verification failed. Please try again.')) } - return { + return ok({ userId: data.user.id, - } + }) }, } } diff --git a/src/core/domains/billing/repository.server.ts b/src/core/domains/billing/repository.server.ts index 882869548..d9f68216f 100644 --- a/src/core/domains/billing/repository.server.ts +++ b/src/core/domains/billing/repository.server.ts @@ -2,6 +2,7 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import type { AddOnOrderConfirmResponse, @@ -18,10 +19,7 @@ type BillingRepositoryDeps = { billingApiUrl: string } -export interface BillingScope { - accessToken: string - teamId: string -} +export type BillingScope = TeamRequestScope export interface BillingRepository { createCheckout(tierId: string): Promise> diff --git a/src/core/domains/builds/repository.server.ts b/src/core/domains/builds/repository.server.ts index 91c557d24..c7db9d607 100644 --- a/src/core/domains/builds/repository.server.ts +++ b/src/core/domains/builds/repository.server.ts @@ -1,15 +1,15 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' import type { BuildStatus, ListedBuildModel, RunningBuildStatusModel, } from '@/core/domains/builds/models' -import { - handleDashboardApiError, - handleInfraApiError, -} from '@/core/server/adapters/trpc-errors' +import { l } from '@/core/shared/clients/logger/logger' import { INITIAL_BUILD_STATUSES } from '@/features/dashboard/templates/builds/constants' import { api, infra } from '@/lib/clients/api' import type { components as InfraComponents } from '@/types/infra-api.types' @@ -20,28 +20,27 @@ type BuildsRepositoryDeps = { authHeaders: typeof SUPABASE_AUTH_HEADERS } -export interface BuildsScope { - accessToken: string - teamId: string -} +export type BuildsScope = TeamRequestScope export interface BuildsRepository { listBuilds( buildIdOrTemplate?: string, statuses?: BuildStatus[], options?: ListBuildsOptions - ): Promise - getRunningStatuses(buildIds: string[]): Promise - getBuildInfo(buildId: string): Promise + ): Promise> + getRunningStatuses( + buildIds: string[] + ): Promise> + getBuildInfo(buildId: string): Promise> getInfraBuildStatus( templateId: string, buildId: string - ): Promise + ): Promise> getInfraBuildLogs( templateId: string, buildId: string, options?: GetInfraBuildLogsOptions - ): Promise + ): Promise> } const LIST_BUILDS_DEFAULT_LIMIT = 50 @@ -93,7 +92,7 @@ export function createBuildsRepository( buildIdOrTemplate, statuses = INITIAL_BUILD_STATUSES, options = {} - ): Promise { + ): Promise> { const limit = normalizeListBuildsLimit(options.limit) const result = await deps.apiClient.GET('/builds', { params: { @@ -110,24 +109,33 @@ export function createBuildsRepository( }) if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, + l.error({ + key: 'repositories:builds:list_builds:dashboard_api_error', error: result.error, - teamId: scope.teamId, - path: '/builds', - logKey: 'repositories:builds:list_builds:dashboard_api_error', + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/builds', + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch builds', + result.error + ) + ) } const builds = result.data?.data ?? [] if (builds.length === 0) { - return { + return ok({ data: [], nextCursor: null, - } + }) } - return { + return ok({ data: builds.map( (build): ListedBuildModel => ({ id: build.id, @@ -142,11 +150,11 @@ export function createBuildsRepository( }) ), nextCursor: result.data?.nextCursor ?? null, - } + }) }, async getRunningStatuses(buildIds) { if (buildIds.length === 0) { - return [] + return ok([]) } const result = await deps.apiClient.GET('/builds/statuses', { @@ -161,22 +169,32 @@ export function createBuildsRepository( }) if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, + l.error({ + key: 'repositories:builds:get_running_statuses:dashboard_api_error', error: result.error, - teamId: scope.teamId, - path: '/builds/statuses', - logKey: - 'repositories:builds:get_running_statuses:dashboard_api_error', + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/builds/statuses', + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch build statuses', + result.error + ) + ) } - return (result.data?.buildStatuses ?? []).map((row) => ({ - id: row.id, - status: row.status, - finishedAt: row.finishedAt ? new Date(row.finishedAt).getTime() : null, - statusMessage: row.statusMessage, - })) + return ok( + (result.data?.buildStatuses ?? []).map((row) => ({ + id: row.id, + status: row.status, + finishedAt: row.finishedAt ? new Date(row.finishedAt).getTime() : null, + statusMessage: row.statusMessage, + })) + ) }, async getBuildInfo(buildId) { const result = await deps.apiClient.GET('/builds/{build_id}', { @@ -191,21 +209,28 @@ export function createBuildsRepository( }) if (!result.response.ok || result.error) { - handleDashboardApiError({ - status: result.response.status, + l.error({ + key: 'repositories:builds:get_build_info:dashboard_api_error', error: result.error, - teamId: scope.teamId, - path: '/builds/{build_id}', - logKey: 'repositories:builds:get_build_info:dashboard_api_error', + team_id: scope.teamId, context: { + status: result.response.status, + path: '/builds/{build_id}', build_id: buildId, }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch build info', + result.error + ) + ) } const data = result.data - return { + return ok({ names: data.names ?? null, createdAt: new Date(data.createdAt).getTime(), finishedAt: data.finishedAt @@ -213,7 +238,7 @@ export function createBuildsRepository( : null, status: data.status, statusMessage: data.statusMessage, - } + }) }, async getInfraBuildStatus(templateId, buildId) { const result = await deps.infraClient.GET( @@ -235,16 +260,25 @@ export function createBuildsRepository( ) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:builds:get_build_status:infra_error', error: result.error, - teamId: scope.teamId, - path: '/templates/{templateID}/builds/{buildID}/status', - logKey: 'repositories:builds:get_build_status:infra_error', + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/templates/{templateID}/builds/{buildID}/status', + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch build status', + result.error + ) + ) } - return result.data + return ok(result.data) }, async getInfraBuildLogs(templateId, buildId, options = {}) { const result = await deps.infraClient.GET( @@ -269,16 +303,25 @@ export function createBuildsRepository( ) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:builds:get_build_logs:infra_error', error: result.error, - teamId: scope.teamId, - path: '/templates/{templateID}/builds/{buildID}/logs', - logKey: 'repositories:builds:get_build_logs:infra_error', + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/templates/{templateID}/builds/{buildID}/logs', + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch build logs', + result.error + ) + ) } - return result.data + return ok(result.data) }, } } diff --git a/src/core/domains/keys/repository.server.ts b/src/core/domains/keys/repository.server.ts index 5e014a179..c85a9a26f 100644 --- a/src/core/domains/keys/repository.server.ts +++ b/src/core/domains/keys/repository.server.ts @@ -2,6 +2,7 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { infra } from '@/lib/clients/api' import type { CreatedTeamAPIKey, TeamAPIKey } from '@/types/api.types' @@ -11,10 +12,7 @@ type KeysRepositoryDeps = { authHeaders: typeof SUPABASE_AUTH_HEADERS } -export interface KeysScope { - accessToken: string - teamId: string -} +export type KeysScope = TeamRequestScope export interface KeysRepository { listTeamApiKeys(): Promise> diff --git a/src/core/domains/sandboxes/repository.server.ts b/src/core/domains/sandboxes/repository.server.ts index af9dd4200..10b1f3ac6 100644 --- a/src/core/domains/sandboxes/repository.server.ts +++ b/src/core/domains/sandboxes/repository.server.ts @@ -1,13 +1,10 @@ import 'server-only' -import { TRPCError } from '@trpc/server' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' import type { SandboxEventModel } from '@/core/domains/sandboxes/models' -import { - apiError, - handleDashboardApiError, - handleInfraApiError, -} from '@/core/server/adapters/trpc-errors' import { api, infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import type { @@ -24,10 +21,7 @@ type SandboxesRepositoryDeps = { authHeaders: typeof SUPABASE_AUTH_HEADERS } -export interface SandboxesRequestScope { - accessToken: string - teamId: string -} +export type SandboxesRequestScope = TeamRequestScope export interface GetSandboxLogsOptions { cursor?: number @@ -46,35 +40,42 @@ export interface SandboxesRepository { getSandboxLogs( sandboxId: string, options?: GetSandboxLogsOptions - ): Promise + ): Promise> getSandboxDetails(sandboxId: string): Promise< - | { - source: 'infra' - details: InfraComponents['schemas']['SandboxDetail'] - } - | { - source: 'database-record' - details: DashboardComponents['schemas']['SandboxRecord'] - } + RepoResult< + | { + source: 'infra' + details: InfraComponents['schemas']['SandboxDetail'] + } + | { + source: 'database-record' + details: DashboardComponents['schemas']['SandboxRecord'] + } + > > - getSandboxLifecycleEvents(sandboxId: string): Promise + getSandboxLifecycleEvents( + sandboxId: string + ): Promise> getSandboxMetrics( sandboxId: string, options: GetSandboxMetricsOptions - ): Promise - listSandboxes(): Promise - getSandboxesMetrics(sandboxIds: string[]): Promise + ): Promise> + listSandboxes(): Promise> + getSandboxesMetrics( + sandboxIds: string[] + ): Promise> getTeamMetricsRange( startUnixSeconds: number, endUnixSeconds: number - ): Promise + ): Promise> getTeamMetricsMax( startUnixSeconds: number, endUnixSeconds: number, metric: 'concurrent_sandboxes' | 'sandbox_start_rate' - ): Promise + ): Promise> } +const SANDBOX_NOT_FOUND_MESSAGE = "Sandbox not found or you don't have access to it" const SANDBOX_EVENTS_PAGE_SIZE = 100 const SANDBOX_EVENTS_MAX_PAGES = 50 const SANDBOX_LIFECYCLE_EVENT_PREFIX = 'sandbox.lifecycle.' @@ -127,17 +128,18 @@ export function createSandboxesRepository( `failed to fetch /v2/sandboxes/{sandboxID}/logs: ${result.error?.message || 'Unknown error'}` ) - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) - } - - throw apiError(status) + return err( + repoErrorFromHttp( + status, + status === 404 + ? SANDBOX_NOT_FOUND_MESSAGE + : (result.error?.message ?? 'Failed to fetch sandbox logs'), + result.error + ) + ) } - return result.data + return ok(result.data) }, async getSandboxDetails(sandboxId) { const infraResult = await deps.infraClient.GET('/sandboxes/{sandboxID}', { @@ -153,25 +155,32 @@ export function createSandboxesRepository( }) if (infraResult.response.ok && infraResult.data) { - return { + return ok({ source: 'infra' as const, details: infraResult.data, - } + }) } const infraStatus = infraResult.response.status if (infraStatus !== 404) { - handleInfraApiError({ - status: infraStatus, + l.error({ + key: 'repositories:sandboxes:get_sandbox_details:infra_error', error: infraResult.error, - teamId: scope.teamId, - path: '/sandboxes/{sandboxID}', - logKey: 'repositories:sandboxes:get_sandbox_details:infra_error', + team_id: scope.teamId, context: { + status: infraStatus, + path: '/sandboxes/{sandboxID}', sandbox_id: sandboxId, }, }) + return err( + repoErrorFromHttp( + infraStatus, + infraResult.error?.message ?? 'Failed to fetch sandbox details', + infraResult.error + ) + ) } const dashboardResult = await deps.apiClient.GET( @@ -190,32 +199,36 @@ export function createSandboxesRepository( ) if (dashboardResult.response.ok && dashboardResult.data) { - return { + return ok({ source: 'database-record' as const, details: dashboardResult.data, - } + }) } const dashboardStatus = dashboardResult.response.status if (dashboardStatus === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) + return err(repoErrorFromHttp(404, SANDBOX_NOT_FOUND_MESSAGE)) } - handleDashboardApiError({ - status: dashboardStatus, + l.error({ + key: 'repositories:sandboxes:get_sandbox_details:fallback_error', error: dashboardResult.error, - teamId: scope.teamId, - path: '/sandboxes/{sandboxID}/record', - logKey: 'repositories:sandboxes:get_sandbox_details:fallback_error', + team_id: scope.teamId, context: { + status: dashboardStatus, + path: '/sandboxes/{sandboxID}/record', infra_status: infraStatus, sandbox_id: sandboxId, }, }) + return err( + repoErrorFromHttp( + dashboardStatus, + dashboardResult.error?.message ?? 'Failed to fetch sandbox details', + dashboardResult.error + ) + ) }, async getSandboxLifecycleEvents(sandboxId) { const lifecycleEvents: SandboxEventModel[] = [] @@ -288,7 +301,7 @@ export function createSandboxesRepository( } } - return lifecycleEvents + return ok(lifecycleEvents) }, async getSandboxMetrics(sandboxId, options) { const startUnixSeconds = Math.floor(options.startUnixMs / 1000) @@ -329,17 +342,18 @@ export function createSandboxesRepository( `failed to fetch /sandboxes/{sandboxID}/metrics: ${result.error?.message || 'Unknown error'}` ) - if (status === 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Sandbox not found or you don't have access to it", - }) - } - - throw apiError(status) + return err( + repoErrorFromHttp( + status, + status === 404 + ? SANDBOX_NOT_FOUND_MESSAGE + : (result.error?.message ?? 'Failed to fetch sandbox metrics'), + result.error + ) + ) } - return result.data + return ok(result.data) }, async listSandboxes() { const result = await deps.infraClient.GET('/sandboxes', { @@ -350,16 +364,25 @@ export function createSandboxesRepository( }) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:sandboxes:list_sandboxes:infra_error', error: result.error, - teamId: scope.teamId, - path: '/sandboxes', - logKey: 'repositories:sandboxes:list_sandboxes:infra_error', + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/sandboxes', + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to list sandboxes', + result.error + ) + ) } - return result.data + return ok(result.data) }, async getSandboxesMetrics(sandboxIds) { const result = await deps.infraClient.GET('/sandboxes/metrics', { @@ -375,17 +398,26 @@ export function createSandboxesRepository( }) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:sandboxes:get_sandboxes_metrics:infra_error', error: result.error, - teamId: scope.teamId, - path: '/sandboxes/metrics', - logKey: 'repositories:sandboxes:get_sandboxes_metrics:infra_error', - context: { sandbox_ids: sandboxIds }, + team_id: scope.teamId, + context: { + status: result.response.status, + path: '/sandboxes/metrics', + sandbox_ids: sandboxIds, + }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch sandboxes metrics', + result.error + ) + ) } - return result.data.sandboxes + return ok(result.data.sandboxes) }, async getTeamMetricsRange(startUnixSeconds, endUnixSeconds) { const result = await deps.infraClient.GET('/teams/{teamID}/metrics', { @@ -405,20 +437,27 @@ export function createSandboxesRepository( }) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:sandboxes:get_team_metrics:infra_error', error: result.error, - teamId: scope.teamId, - path: '/teams/{teamID}/metrics', - logKey: 'repositories:sandboxes:get_team_metrics:infra_error', + team_id: scope.teamId, context: { + status: result.response.status, + path: '/teams/{teamID}/metrics', start_unix_seconds: startUnixSeconds, end_unix_seconds: endUnixSeconds, }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch team metrics', + result.error + ) + ) } - return result.data + return ok(result.data) }, async getTeamMetricsMax(startUnixSeconds, endUnixSeconds, metric) { const result = await deps.infraClient.GET('/teams/{teamID}/metrics/max', { @@ -439,21 +478,28 @@ export function createSandboxesRepository( }) if (!result.response.ok || result.error) { - handleInfraApiError({ - status: result.response.status, + l.error({ + key: 'repositories:sandboxes:get_team_metrics_max:infra_error', error: result.error, - teamId: scope.teamId, - path: '/teams/{teamID}/metrics/max', - logKey: 'repositories:sandboxes:get_team_metrics_max:infra_error', + team_id: scope.teamId, context: { + status: result.response.status, + path: '/teams/{teamID}/metrics/max', start_unix_seconds: startUnixSeconds, end_unix_seconds: endUnixSeconds, metric, }, }) + return err( + repoErrorFromHttp( + result.response.status, + result.error?.message ?? 'Failed to fetch team metrics max', + result.error + ) + ) } - return result.data + return ok(result.data) }, } } diff --git a/src/core/domains/support/repository.server.ts b/src/core/domains/support/repository.server.ts index 104434fea..d300a7575 100644 --- a/src/core/domains/support/repository.server.ts +++ b/src/core/domains/support/repository.server.ts @@ -1,8 +1,10 @@ import 'server-only' import { AttachmentType, PlainClient } from '@team-plain/typescript-sdk' -import { TRPCError } from '@trpc/server' -import { createTeamsRepository } from '@/core/domains/teams/repository.server' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' import { l } from '@/lib/clients/logger/logger' const MAX_FILE_SIZE = 10 * 1024 * 1024 @@ -18,17 +20,16 @@ type SupportRepositoryDeps = { createPlainClient: () => PlainClient } -export interface SupportScope { - accessToken: string - teamId?: string -} +export type SupportScope = TeamRequestScope export interface SupportRepository { - getTeamSupportData(): Promise<{ - name: string - email: string - tier: string - }> + getTeamSupportData(): Promise< + RepoResult<{ + name: string + email: string + tier: string + }> + > createSupportThread(input: { description: string files?: FileInput[] @@ -37,7 +38,7 @@ export interface SupportRepository { customerEmail: string accountOwnerEmail: string customerTier: string - }): Promise<{ threadId: string }> + }): Promise> } function formatThreadText(input: { @@ -124,19 +125,11 @@ export function createSupportRepository( }), } ): SupportRepository { - const requireTeamId = (teamId?: string): string => { - if (!teamId) { - throw new Error('teamId is required in request scope') - } - return teamId - } - return { async getTeamSupportData() { - const teamResult = await createTeamsRepository({ + const teamResult = await createUserTeamsRepository({ accessToken: scope.accessToken, - teamId: requireTeamId(scope.teamId), - }).getCurrentUserTeam(requireTeamId(scope.teamId)) + }).getCurrentUserTeam(scope.teamId) if (!teamResult.ok) { l.error( @@ -147,22 +140,16 @@ export function createSupportRepository( }, 'failed to fetch team data' ) - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to load team information', - }) + return err(teamResult.error) } const team = teamResult.data - return { name: team.name, email: team.email, tier: team.tier } + return ok({ name: team.name, email: team.email, tier: team.tier }) }, async createSupportThread(input) { if (!process.env.PLAIN_API_KEY) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Support API not configured', - }) + return err(repoErrorFromHttp(500, 'Support API not configured')) } const { @@ -191,10 +178,13 @@ export function createSupportRepository( }) if (customerResult.error) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to create support ticket', - }) + return err( + repoErrorFromHttp( + 500, + 'Failed to create support ticket', + customerResult.error + ) + ) } const customerId = customerResult.data.customer.id @@ -237,13 +227,16 @@ export function createSupportRepository( }) if (result.error) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to create support ticket', - }) + return err( + repoErrorFromHttp( + 500, + 'Failed to create support ticket', + result.error + ) + ) } - return { threadId: result.data.id } + return ok({ threadId: result.data.id }) }, } } diff --git a/src/core/domains/teams/repository.server.ts b/src/core/domains/teams/teams-repository.server.ts similarity index 55% rename from src/core/domains/teams/repository.server.ts rename to src/core/domains/teams/teams-repository.server.ts index 7843bc083..d4e9b2fbe 100644 --- a/src/core/domains/teams/repository.server.ts +++ b/src/core/domains/teams/teams-repository.server.ts @@ -2,19 +2,15 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { api } from '@/lib/clients/api' import { supabaseAdmin } from '@/lib/clients/supabase/admin' import type { components as DashboardComponents } from '@/types/dashboard-api.types' -import type { ClientTeam, ResolvedTeam, TeamLimits, TeamMember } from './models' +import type { ClientTeam, TeamLimits, TeamMember } from './models' type ApiUserTeam = { id: string - name: string - slug: string - tier: string - email: string - isDefault: boolean limits: { concurrentSandboxes: number diskMb: number @@ -24,42 +20,16 @@ type ApiUserTeam = { } } -function mapApiTeamToClientTeam(apiTeam: ApiUserTeam): ClientTeam { - return { - id: apiTeam.id, - name: apiTeam.name, - slug: apiTeam.slug, - tier: apiTeam.tier, - email: apiTeam.email, - is_default: apiTeam.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: null, - } -} - type TeamsRepositoryDeps = { apiClient: typeof api authHeaders: typeof SUPABASE_AUTH_HEADERS adminClient: typeof supabaseAdmin } -export interface TeamsRequestScope { - accessToken: string - teamId?: string -} +export type TeamsRequestScope = TeamRequestScope export interface TeamsRepository { - listUserTeams(): Promise> - getCurrentUserTeam(teamIdOrSlug: string): Promise> - resolveTeamBySlug( - slug: string, - next?: { tags?: string[] } - ): Promise> - getTeamLimitsByIdOrSlug(teamIdOrSlug: string): Promise> + getTeamLimits(): Promise> listTeamMembers(): Promise> updateTeamName( name: string @@ -79,108 +49,24 @@ export function createTeamsRepository( adminClient: supabaseAdmin, } ): TeamsRepository { - const requireTeamId = (teamId?: string): string => { - if (!teamId) { - throw new Error('teamId is required in request scope') - } - return teamId - } - - const listApiUserTeams = async ( - accessToken: string - ): Promise> => { - const { data, error, response } = await deps.apiClient.GET('/teams', { - headers: deps.authHeaders(accessToken), - }) - - if (!response.ok || error || !data?.teams) { - return err( - repoErrorFromHttp( - response.status, - error?.message ?? 'Failed to fetch user teams', - error - ) - ) - } - - return ok(data.teams as ApiUserTeam[]) - } - return { - async listUserTeams(): Promise> { - const teamsResult = await listApiUserTeams(scope.accessToken) - - if (!teamsResult.ok) { - return teamsResult - } - - return ok(teamsResult.data.map(mapApiTeamToClientTeam)) - }, - async getCurrentUserTeam( - teamIdOrSlug: string - ): Promise> { - const teamsResult = await listApiUserTeams(scope.accessToken) - - if (!teamsResult.ok) { - return teamsResult - } - - const team = teamsResult.data.find( - (candidate) => - candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug - ) - - if (!team) { - return err( - repoErrorFromHttp(403, 'Team not found or access denied', { - teamIdOrSlug, - }) - ) - } - - return ok(mapApiTeamToClientTeam(team)) - }, - async resolveTeamBySlug( - slug: string, - next?: { tags?: string[] } - ): Promise> { - const { data, error, response } = await deps.apiClient.GET( - '/teams/resolve', - { - params: { query: { slug } }, - headers: deps.authHeaders(scope.accessToken), - next, - } - ) + async getTeamLimits(): Promise> { + const { data, error, response } = await deps.apiClient.GET('/teams', { + headers: deps.authHeaders(scope.accessToken, scope.teamId), + }) - if (!response.ok || error || !data) { + if (!response.ok || error || !data?.teams) { return err( repoErrorFromHttp( response.status, - error?.message ?? 'Failed to resolve team', + error?.message ?? 'Failed to fetch team limits', error ) ) } - return ok({ - id: data.id, - slug: data.slug, - }) - }, - async getTeamLimitsByIdOrSlug( - teamIdOrSlug: string - ): Promise> { - const teamsResult = await listApiUserTeams(scope.accessToken) - - if (!teamsResult.ok) { - return teamsResult - } - - const team = teamsResult.data.find( - (candidate) => - candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug - ) + const teams = data.teams as ApiUserTeam[] + const team = teams.find((candidate) => candidate.id === scope.teamId) if (!team) { return err(repoErrorFromHttp(404, 'Team not found')) @@ -195,12 +81,11 @@ export function createTeamsRepository( }) }, async listTeamMembers(): Promise> { - const teamId = requireTeamId(scope.teamId) const { data, error, response } = await deps.apiClient.GET( '/teams/{teamId}/members', { - params: { path: { teamId } }, - headers: deps.authHeaders(scope.accessToken, teamId), + params: { path: { teamId: scope.teamId } }, + headers: deps.authHeaders(scope.accessToken, scope.teamId), } ) @@ -243,12 +128,11 @@ export function createTeamsRepository( ): Promise< RepoResult > { - const teamId = requireTeamId(scope.teamId) const { data, error, response } = await deps.apiClient.PATCH( '/teams/{teamId}', { - params: { path: { teamId } }, - headers: deps.authHeaders(scope.accessToken, teamId), + params: { path: { teamId: scope.teamId } }, + headers: deps.authHeaders(scope.accessToken, scope.teamId), body: { name }, } ) @@ -266,12 +150,11 @@ export function createTeamsRepository( return ok(data) }, async addTeamMember(email): Promise> { - const teamId = requireTeamId(scope.teamId) const { error, response } = await deps.apiClient.POST( '/teams/{teamId}/members', { - params: { path: { teamId } }, - headers: deps.authHeaders(scope.accessToken, teamId), + params: { path: { teamId: scope.teamId } }, + headers: deps.authHeaders(scope.accessToken, scope.teamId), body: { email }, } ) @@ -289,12 +172,11 @@ export function createTeamsRepository( return ok(undefined) }, async removeTeamMember(userId): Promise> { - const teamId = requireTeamId(scope.teamId) const { error, response } = await deps.apiClient.DELETE( '/teams/{teamId}/members/{userId}', { - params: { path: { teamId, userId } }, - headers: deps.authHeaders(scope.accessToken, teamId), + params: { path: { teamId: scope.teamId, userId } }, + headers: deps.authHeaders(scope.accessToken, scope.teamId), } ) @@ -313,11 +195,10 @@ export function createTeamsRepository( async updateTeamProfilePictureUrl( profilePictureUrl ): Promise> { - const teamId = requireTeamId(scope.teamId) const { data, error } = await deps.adminClient .from('teams') .update({ profile_picture_url: profilePictureUrl }) - .eq('id', teamId) + .eq('id', scope.teamId) .select() .single() diff --git a/src/core/domains/teams/user-teams-repository.server.ts b/src/core/domains/teams/user-teams-repository.server.ts new file mode 100644 index 000000000..657b3b3c6 --- /dev/null +++ b/src/core/domains/teams/user-teams-repository.server.ts @@ -0,0 +1,147 @@ +import 'server-only' + +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { RequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' +import { api } from '@/lib/clients/api' +import type { ClientTeam, ResolvedTeam } from './models' + +type ApiUserTeam = { + id: string + name: string + slug: string + tier: string + email: string + isDefault: boolean + limits: { + concurrentSandboxes: number + diskMb: number + maxLengthHours: number + maxRamMb: number + maxVcpu: number + } +} + +function mapApiTeamToClientTeam(apiTeam: ApiUserTeam): ClientTeam { + return { + id: apiTeam.id, + name: apiTeam.name, + slug: apiTeam.slug, + tier: apiTeam.tier, + email: apiTeam.email, + is_default: apiTeam.isDefault, + is_banned: false, + is_blocked: false, + blocked_reason: null, + cluster_id: null, + created_at: '', + profile_picture_url: null, + } +} + +type UserTeamsRepositoryDeps = { + apiClient: typeof api + authHeaders: typeof SUPABASE_AUTH_HEADERS +} + +export type UserTeamsRequestScope = RequestScope + +export interface UserTeamsRepository { + listUserTeams(): Promise> + getCurrentUserTeam(teamIdOrSlug: string): Promise> + resolveTeamBySlug( + slug: string, + next?: { tags?: string[] } + ): Promise> +} + +export function createUserTeamsRepository( + scope: UserTeamsRequestScope, + deps: UserTeamsRepositoryDeps = { + apiClient: api, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): UserTeamsRepository { + const listApiUserTeams = async (): Promise> => { + const { data, error, response } = await deps.apiClient.GET('/teams', { + headers: deps.authHeaders(scope.accessToken), + }) + + if (!response.ok || error || !data?.teams) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to fetch user teams', + error + ) + ) + } + + return ok(data.teams as ApiUserTeam[]) + } + + return { + async listUserTeams(): Promise> { + const teamsResult = await listApiUserTeams() + + if (!teamsResult.ok) { + return teamsResult + } + + return ok(teamsResult.data.map(mapApiTeamToClientTeam)) + }, + async getCurrentUserTeam( + teamIdOrSlug: string + ): Promise> { + const teamsResult = await listApiUserTeams() + + if (!teamsResult.ok) { + return teamsResult + } + + const team = teamsResult.data.find( + (candidate) => + candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug + ) + + if (!team) { + return err( + repoErrorFromHttp(403, 'Team not found or access denied', { + teamIdOrSlug, + }) + ) + } + + return ok(mapApiTeamToClientTeam(team)) + }, + async resolveTeamBySlug( + slug: string, + next?: { tags?: string[] } + ): Promise> { + const { data, error, response } = await deps.apiClient.GET( + '/teams/resolve', + { + params: { query: { slug } }, + headers: deps.authHeaders(scope.accessToken), + next, + } + ) + + if (!response.ok || error || !data) { + return err( + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to resolve team', + error + ) + ) + } + + return ok({ + id: data.id, + slug: data.slug, + }) + }, + } +} diff --git a/src/core/domains/templates/repository.server.ts b/src/core/domains/templates/repository.server.ts index 9fae7bf4e..8a9be16df 100644 --- a/src/core/domains/templates/repository.server.ts +++ b/src/core/domains/templates/repository.server.ts @@ -8,6 +8,10 @@ import { MOCK_TEMPLATES_DATA, } from '@/configs/mock-data' import { repoErrorFromHttp } from '@/core/shared/errors' +import type { + RequestScope, + TeamRequestScope, +} from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { api, infra } from '@/lib/clients/api' import type { DefaultTemplate, Template } from '@/types/api.types' @@ -18,16 +22,8 @@ type TemplatesRepositoryDeps = { authHeaders: typeof SUPABASE_AUTH_HEADERS } -export interface TemplatesScope { - accessToken: string - teamId?: string -} - -export interface TemplatesRepository { +export interface TeamTemplatesRepository { getTeamTemplates(): Promise> - getDefaultTemplatesCached(): Promise< - RepoResult<{ templates: DefaultTemplate[] }> - > deleteTemplate(templateId: string): Promise> updateTemplateVisibility( templateId: string, @@ -35,21 +31,20 @@ export interface TemplatesRepository { ): Promise> } +export interface DefaultTemplatesRepository { + getDefaultTemplatesCached(): Promise< + RepoResult<{ templates: DefaultTemplate[] }> + > +} + export function createTemplatesRepository( - scope: TemplatesScope, + scope: TeamRequestScope, deps: TemplatesRepositoryDeps = { apiClient: api, infraClient: infra, authHeaders: SUPABASE_AUTH_HEADERS, } -): TemplatesRepository { - const requireTeamId = (teamId?: string): string => { - if (!teamId) { - throw new Error('teamId is required in request scope') - } - return teamId - } - +): TeamTemplatesRepository { return { async getTeamTemplates() { if (USE_MOCK_DATA) { @@ -57,15 +52,14 @@ export function createTemplatesRepository( templates: MOCK_TEMPLATES_DATA, }) } - const res = await deps.infraClient.GET('/templates', { params: { query: { - teamID: requireTeamId(scope.teamId), + teamID: scope.teamId, }, }, headers: { - ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), + ...deps.authHeaders(scope.accessToken, scope.teamId), }, }) @@ -83,6 +77,68 @@ export function createTemplatesRepository( templates: res.data, }) }, + async deleteTemplate(templateId) { + const res = await deps.infraClient.DELETE('/templates/{templateID}', { + params: { + path: { + templateID: templateId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to delete template', + res.error + ) + ) + } + + return ok({ success: true as const }) + }, + async updateTemplateVisibility(templateId, isPublic) { + const res = await deps.infraClient.PATCH('/templates/{templateID}', { + body: { + public: isPublic, + }, + params: { + path: { + templateID: templateId, + }, + }, + headers: { + ...deps.authHeaders(scope.accessToken, scope.teamId), + }, + }) + + if (!res.response.ok || res.error) { + return err( + repoErrorFromHttp( + res.response.status, + res.error?.message ?? 'Failed to update template', + res.error + ) + ) + } + + return ok({ success: true as const, public: isPublic }) + }, + } +} + +export function createDefaultTemplatesRepository( + scope: RequestScope, + deps: Pick = { + apiClient: api, + authHeaders: SUPABASE_AUTH_HEADERS, + } +): DefaultTemplatesRepository { + return { async getDefaultTemplatesCached() { if (USE_MOCK_DATA) { return ok({ @@ -138,56 +194,5 @@ export function createTemplatesRepository( return ok({ templates }) }, - async deleteTemplate(templateId) { - const res = await deps.infraClient.DELETE('/templates/{templateID}', { - params: { - path: { - templateID: templateId, - }, - }, - headers: { - ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), - }, - }) - - if (!res.response.ok || res.error) { - return err( - repoErrorFromHttp( - res.response.status, - res.error?.message ?? 'Failed to delete template', - res.error - ) - ) - } - - return ok({ success: true as const }) - }, - async updateTemplateVisibility(templateId, isPublic) { - const res = await deps.infraClient.PATCH('/templates/{templateID}', { - body: { - public: isPublic, - }, - params: { - path: { - templateID: templateId, - }, - }, - headers: { - ...deps.authHeaders(scope.accessToken, requireTeamId(scope.teamId)), - }, - }) - - if (!res.response.ok || res.error) { - return err( - repoErrorFromHttp( - res.response.status, - res.error?.message ?? 'Failed to update template', - res.error - ) - ) - } - - return ok({ success: true as const, public: isPublic }) - }, } } diff --git a/src/core/domains/webhooks/repository.server.ts b/src/core/domains/webhooks/repository.server.ts index 2aab48b72..bb17c06bc 100644 --- a/src/core/domains/webhooks/repository.server.ts +++ b/src/core/domains/webhooks/repository.server.ts @@ -2,6 +2,7 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { infra } from '@/lib/clients/api' import type { components as ArgusComponents } from '@/types/argus-api.types' @@ -11,10 +12,7 @@ type WebhooksRepositoryDeps = { authHeaders: typeof SUPABASE_AUTH_HEADERS } -export interface WebhooksScope { - accessToken: string - teamId: string -} +export type WebhooksScope = TeamRequestScope export interface UpsertWebhookInput { mode: 'create' | 'edit' diff --git a/src/core/server/actions/client.ts b/src/core/server/actions/client.ts index 1c87ae9a0..ad6e80656 100644 --- a/src/core/server/actions/client.ts +++ b/src/core/server/actions/client.ts @@ -4,12 +4,12 @@ import { unauthorized } from 'next/navigation' import { createMiddleware, createSafeActionClient } from 'next-safe-action' import { serializeError } from 'serialize-error' import { z } from 'zod' -import { - createRequestContext, - type RequestContextServices, -} from '@/core/server/context/request-context' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' +import type { + RequestScope, + TeamRequestScope, +} from '@/core/shared/repository-scope' import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' import { UnauthenticatedError, UnknownError } from '@/core/shared/errors' import { l } from '@/core/shared/clients/logger/logger' @@ -17,6 +17,18 @@ import { createClient } from '@/core/shared/clients/supabase/server' import { getTracer } from '@/core/shared/clients/tracer' import { ActionError, flattenClientInputValue } from './utils' +type SupabaseServerClient = Awaited> + +export interface AuthActionContext { + user: User + session: Session + supabase: SupabaseServerClient +} + +export interface TeamActionContext extends AuthActionContext { + teamId: string +} + export const actionClient = createSafeActionClient({ handleServerError(e) { const s = trace.getActiveSpan() @@ -164,20 +176,12 @@ export const authActionClient = actionClient.use(async ({ next }) => { user, session, supabase, - services: createRequestContext({ - accessToken: session.access_token, - }).services, }, }) }) export const withTeamIdResolution = createMiddleware<{ - ctx: { - user: User - session: Session - supabase: Awaited> - services: RequestContextServices - } + ctx: AuthActionContext }>().define(async ({ next, clientInput, ctx }) => { if ( !clientInput || @@ -222,10 +226,51 @@ export const withTeamIdResolution = createMiddleware<{ return next({ ctx: { teamId, - services: createRequestContext({ - accessToken: ctx.session.access_token, - teamId, - }).services, }, }) }) + +export function withAuthedRequestRepository< + TRepository, + TContextExtension extends object, +>( + createRepository: (scope: RequestScope) => TRepository, + extendContext: (repository: TRepository) => TContextExtension +) { + return createMiddleware<{ + ctx: AuthActionContext + }>().define(async ({ next, ctx }) => { + const repository = createRepository({ + accessToken: ctx.session.access_token, + }) + + return next({ + ctx: { + ...extendContext(repository), + }, + }) + }) +} + +export function withTeamAuthedRequestRepository< + TRepository, + TContextExtension extends object, +>( + createRepository: (scope: TeamRequestScope) => TRepository, + extendContext: (repository: TRepository) => TContextExtension +) { + return createMiddleware<{ + ctx: TeamActionContext + }>().define(async ({ next, ctx }) => { + const repository = createRepository({ + accessToken: ctx.session.access_token, + teamId: ctx.teamId, + }) + + return next({ + ctx: { + ...extendContext(repository), + }, + }) + }) +} diff --git a/src/core/server/actions/key-actions.ts b/src/core/server/actions/key-actions.ts index fe50f360f..0a378e806 100644 --- a/src/core/server/actions/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -3,11 +3,23 @@ import { revalidatePath, updateTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { createKeysRepository } from '@/core/domains/keys/repository.server' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' +const withKeysRepository = withTeamAuthedRequestRepository( + createKeysRepository, + (keysRepository) => ({ + keysRepository, + }) +) + // Create API Key const CreateApiKeySchema = z.object({ @@ -23,10 +35,11 @@ export const createApiKeyAction = authActionClient .schema(CreateApiKeySchema) .metadata({ actionName: 'createApiKey' }) .use(withTeamIdResolution) + .use(withKeysRepository) .action(async ({ parsedInput, ctx }) => { const { name } = parsedInput - const result = await ctx.services.keys.createApiKey(name) + const result = await ctx.keysRepository.createApiKey(name) if (!result.ok) { l.error({ @@ -62,9 +75,10 @@ export const deleteApiKeyAction = authActionClient .schema(DeleteApiKeySchema) .metadata({ actionName: 'deleteApiKey' }) .use(withTeamIdResolution) + .use(withKeysRepository) .action(async ({ parsedInput, ctx }) => { const { apiKeyId } = parsedInput - const result = await ctx.services.keys.deleteApiKey(apiKeyId) + const result = await ctx.keysRepository.deleteApiKey(apiKeyId) if (!result.ok) { l.error({ diff --git a/src/core/server/actions/team-actions.ts b/src/core/server/actions/team-actions.ts index e3face12d..3a4b8e00e 100644 --- a/src/core/server/actions/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -8,25 +8,36 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { zfd } from 'zod-form-data' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' import { CreateTeamSchema, UpdateTeamNameSchema, } from '@/core/domains/teams/schemas' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError, returnServerError } from '@/lib/utils/action' import type { CreateTeamsResponse } from '@/core/domains/billing/models' +const withTeamsRepository = withTeamAuthedRequestRepository( + createTeamsRepository, + (teamsRepository) => ({ teamsRepository }) +) + export const updateTeamNameAction = authActionClient .schema(UpdateTeamNameSchema) .metadata({ actionName: 'updateTeamName' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { const { name, teamIdOrSlug } = parsedInput - const result = await ctx.services.teams.updateTeamName(name) + const result = await ctx.teamsRepository.updateTeamName(name) if (!result.ok) { return toActionErrorFromRepoError(result.error) @@ -46,9 +57,10 @@ export const addTeamMemberAction = authActionClient .schema(AddTeamMemberSchema) .metadata({ actionName: 'addTeamMember' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { const { email, teamIdOrSlug } = parsedInput - const result = await ctx.services.teams.addTeamMember(email) + const result = await ctx.teamsRepository.addTeamMember(email) if (!result.ok) { return toActionErrorFromRepoError(result.error) @@ -66,9 +78,10 @@ export const removeTeamMemberAction = authActionClient .schema(RemoveTeamMemberSchema) .metadata({ actionName: 'removeTeamMember' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { const { userId, teamIdOrSlug } = parsedInput - const result = await ctx.services.teams.removeTeamMember(userId) + const result = await ctx.teamsRepository.removeTeamMember(userId) if (!result.ok) { return toActionErrorFromRepoError(result.error) @@ -120,9 +133,10 @@ export const uploadTeamProfilePictureAction = authActionClient .schema(UploadTeamProfilePictureSchema) .metadata({ actionName: 'uploadTeamProfilePicture' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { const { image, teamIdOrSlug } = parsedInput - const { teamId, services } = ctx + const { teamId, teamsRepository } = ctx const allowedTypes = ['image/jpeg', 'image/png'] @@ -169,7 +183,7 @@ export const uploadTeamProfilePictureAction = authActionClient const publicUrl = await uploadFile(buffer, storagePath, fileType.mime) - const result = await services.teams.updateTeamProfilePictureUrl(publicUrl) + const result = await teamsRepository.updateTeamProfilePictureUrl(publicUrl) if (!result.ok) { throw new Error(result.error.message) } diff --git a/src/core/server/actions/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts index 0e7eac442..14c69115f 100644 --- a/src/core/server/actions/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -3,15 +3,25 @@ import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' import { COOKIE_KEYS } from '@/configs/cookies' +import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { DeleteWebhookSchema, UpdateWebhookSecretSchema, UpsertWebhookSchema, } from '@/core/server/functions/webhooks/schema' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' +const withWebhooksRepository = withTeamAuthedRequestRepository( + createWebhooksRepository, + (webhooksRepository) => ({ webhooksRepository }) +) + // Upsert Webhook (Create or Update) // NOTE: we combine insert and edit for now, since @@ -21,12 +31,13 @@ export const upsertWebhookAction = authActionClient .schema(UpsertWebhookSchema) .metadata({ actionName: 'upsertWebhook' }) .use(withTeamIdResolution) + .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { const { mode, webhookId, name, url, events, signatureSecret, enabled } = parsedInput const { session, teamId } = ctx - const response = await ctx.services.webhooks.upsertWebhook({ + const response = await ctx.webhooksRepository.upsertWebhook({ mode: mode === 'add' ? 'create' : 'edit', webhookId: webhookId ?? undefined, name, @@ -78,11 +89,12 @@ export const deleteWebhookAction = authActionClient .schema(DeleteWebhookSchema) .metadata({ actionName: 'deleteWebhook' }) .use(withTeamIdResolution) + .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { const { webhookId } = parsedInput const { session, teamId } = ctx - const response = await ctx.services.webhooks.deleteWebhook(webhookId) + const response = await ctx.webhooksRepository.deleteWebhook(webhookId) if (!response.ok) { const status = response.error.status @@ -119,11 +131,12 @@ export const updateWebhookSecretAction = authActionClient .schema(UpdateWebhookSecretSchema) .metadata({ actionName: 'updateWebhookSecret' }) .use(withTeamIdResolution) + .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { const { webhookId, signatureSecret } = parsedInput const { session, teamId } = ctx - const response = await ctx.services.webhooks.updateWebhookSecret( + const response = await ctx.webhooksRepository.updateWebhookSecret( webhookId, signatureSecret ) diff --git a/src/core/server/api/middlewares/auth.ts b/src/core/server/api/middlewares/auth.ts index cec9e8f7b..1ab89d1ac 100644 --- a/src/core/server/api/middlewares/auth.ts +++ b/src/core/server/api/middlewares/auth.ts @@ -5,7 +5,6 @@ import { serializeCookieHeader, } from '@supabase/ssr' import { unauthorizedUserError } from '@/core/server/adapters/trpc-errors' -import { createRequestContext } from '@/core/server/context/request-context' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import { t } from '@/core/server/trpc/init' @@ -80,9 +79,6 @@ export const authMiddleware = t.middleware(async ({ ctx, next }) => { ...ctx, session, user, - services: createRequestContext({ - accessToken: session.access_token, - }).services, }, }) } finally { diff --git a/src/core/server/api/middlewares/repository.ts b/src/core/server/api/middlewares/repository.ts new file mode 100644 index 000000000..8f911345f --- /dev/null +++ b/src/core/server/api/middlewares/repository.ts @@ -0,0 +1,54 @@ +import { + forbiddenTeamAccessError, + unauthorizedUserError, +} from '@/core/server/adapters/trpc-errors' +import { t } from '@/core/server/trpc/init' +import type { + RequestScope, + TeamRequestScope, +} from '@/core/shared/repository-scope' + +export function withAuthedRequestRepository< + TRepository, + TContextExtension extends object, +>( + createRepository: (scope: RequestScope) => TRepository, + extendContext: (repository: TRepository) => TContextExtension +) { + return t.middleware(({ ctx, next }) => { + if (!ctx.session) { + throw unauthorizedUserError() + } + + const repository = createRepository({ + accessToken: ctx.session.access_token, + }) + + return next({ ctx: { ...ctx, ...extendContext(repository) } }) + }) +} + +export function withTeamAuthedRequestRepository< + TRepository, + TContextExtension extends object, +>( + createRepository: (scope: TeamRequestScope) => TRepository, + extendContext: (repository: TRepository) => TContextExtension +) { + return t.middleware(({ ctx, next }) => { + if (!ctx.session) { + throw unauthorizedUserError() + } + + if (!ctx.teamId) { + throw forbiddenTeamAccessError() + } + + const repository = createRepository({ + accessToken: ctx.session.access_token, + teamId: ctx.teamId, + }) + + return next({ ctx: { ...ctx, ...extendContext(repository) } }) + }) +} diff --git a/src/core/server/api/routers/billing.ts b/src/core/server/api/routers/billing.ts index 519b72088..46158e771 100644 --- a/src/core/server/api/routers/billing.ts +++ b/src/core/server/api/routers/billing.ts @@ -1,6 +1,9 @@ import { TRPCError } from '@trpc/server' import { headers } from 'next/headers' import { z } from 'zod' +import { createBillingRepository } from '@/core/domains/billing/repository.server' +import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' @@ -13,53 +16,63 @@ function limitTypeToKey(type: 'limit' | 'alert') { return type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte' } +const billingRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository(createBillingRepository, (billingRepository) => ({ + billingRepository, + })) +) + +const billingAndTeamsRepositoryProcedure = billingRepositoryProcedure.use( + withTeamAuthedRequestRepository(createTeamsRepository, (teamsRepository) => ({ + teamsRepository, + })) +) + export const billingRouter = createTRPCRouter({ - createCheckout: protectedTeamProcedure + createCheckout: billingRepositoryProcedure .input(z.object({ tierId: z.string() })) .mutation(async ({ ctx, input }) => { - const result = await ctx.services.billing.createCheckout(input.tierId) + const result = await ctx.billingRepository.createCheckout(input.tierId) if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - createCustomerPortalSession: protectedTeamProcedure.mutation( + createCustomerPortalSession: billingRepositoryProcedure.mutation( async ({ ctx }) => { const origin = (await headers()).get('origin') const result = - await ctx.services.billing.createCustomerPortalSession(origin) + await ctx.billingRepository.createCustomerPortalSession(origin) if (!result.ok) throwTRPCErrorFromRepoError(result.error) return { url: result.data.url } } ), - getItems: protectedTeamProcedure.query(async ({ ctx }) => { - const result = await ctx.services.billing.getItems() + getItems: billingRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.billingRepository.getItems() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - getUsage: protectedTeamProcedure.query(async ({ ctx }) => { - const result = await ctx.services.billing.getUsage() + getUsage: billingRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.billingRepository.getUsage() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - getInvoices: protectedTeamProcedure.query(async ({ ctx }) => { - const result = await ctx.services.billing.getInvoices() + getInvoices: billingRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.billingRepository.getInvoices() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - getLimits: protectedTeamProcedure.query(async ({ ctx }) => { - const result = await ctx.services.billing.getLimits() + getLimits: billingRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.billingRepository.getLimits() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - getTeamLimits: protectedTeamProcedure.query(async ({ ctx }) => { - const limitsResult = await ctx.services.teams.getTeamLimitsByIdOrSlug( - ctx.teamId - ) + getTeamLimits: billingAndTeamsRepositoryProcedure.query(async ({ ctx }) => { + const limitsResult = await ctx.teamsRepository.getTeamLimits() if (!limitsResult.ok) { throwTRPCErrorFromRepoError(limitsResult.error) } @@ -67,7 +80,7 @@ export const billingRouter = createTRPCRouter({ return limitsResult.data }), - setLimit: protectedTeamProcedure + setLimit: billingRepositoryProcedure .input( z.object({ type: z.enum(['limit', 'alert']), @@ -76,35 +89,35 @@ export const billingRouter = createTRPCRouter({ ) .mutation(async ({ ctx, input }) => { const { type, value } = input - const result = await ctx.services.billing.setLimit( + const result = await ctx.billingRepository.setLimit( limitTypeToKey(type), value ) if (!result.ok) throwTRPCErrorFromRepoError(result.error) }), - clearLimit: protectedTeamProcedure + clearLimit: billingRepositoryProcedure .input(z.object({ type: z.enum(['limit', 'alert']) })) .mutation(async ({ ctx, input }) => { const { type } = input - const result = await ctx.services.billing.clearLimit(limitTypeToKey(type)) + const result = await ctx.billingRepository.clearLimit(limitTypeToKey(type)) if (!result.ok) throwTRPCErrorFromRepoError(result.error) }), - createOrder: protectedTeamProcedure + createOrder: billingRepositoryProcedure .input(z.object({ itemId: z.literal(ADDON_500_SANDBOXES_ID) })) .mutation(async ({ ctx, input }) => { const { itemId } = input - const result = await ctx.services.billing.createOrder(itemId) + const result = await ctx.billingRepository.createOrder(itemId) if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - confirmOrder: protectedTeamProcedure + confirmOrder: billingRepositoryProcedure .input(z.object({ orderId: z.string().uuid() })) .mutation(async ({ ctx, input }) => { const { orderId } = input - const result = await ctx.services.billing.confirmOrder(orderId) + const result = await ctx.billingRepository.confirmOrder(orderId) if (!result.ok) { if ( result.error.message.includes( @@ -122,8 +135,8 @@ export const billingRouter = createTRPCRouter({ return result.data }), - getCustomerSession: protectedTeamProcedure.mutation(async ({ ctx }) => { - const result = await ctx.services.billing.getCustomerSession() + getCustomerSession: billingRepositoryProcedure.mutation(async ({ ctx }) => { + const result = await ctx.billingRepository.getCustomerSession() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), diff --git a/src/core/server/api/routers/builds.ts b/src/core/server/api/routers/builds.ts index 6eb46ca5e..be1a7a56d 100644 --- a/src/core/server/api/routers/builds.ts +++ b/src/core/server/api/routers/builds.ts @@ -5,14 +5,23 @@ import { type BuildLogsModel, BuildStatusSchema, } from '@/core/domains/builds/models' +import { createBuildsRepository } from '@/core/domains/builds/repository.server' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' import { LOG_RETENTION_MS } from '@/features/dashboard/templates/builds/constants' +const buildsRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository(createBuildsRepository, (buildsRepository) => ({ + buildsRepository, + })) +) + export const buildsRouter = createTRPCRouter({ // QUERIES - list: protectedTeamProcedure + list: buildsRepositoryProcedure .input( z.object({ buildIdOrTemplate: z.string().optional(), @@ -24,13 +33,22 @@ export const buildsRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const { buildIdOrTemplate, statuses, limit, cursor } = input - return await ctx.services.builds.listBuilds(buildIdOrTemplate, statuses, { - limit, - cursor, - }) + const result = await ctx.buildsRepository.listBuilds( + buildIdOrTemplate, + statuses, + { + limit, + cursor, + } + ) + if (!result.ok) { + throwTRPCErrorFromRepoError(result.error) + } + + return result.data }), - runningStatuses: protectedTeamProcedure + runningStatuses: buildsRepositoryProcedure .input( z.object({ buildIds: z.array(z.string()).max(100), @@ -39,10 +57,15 @@ export const buildsRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const { buildIds } = input - return await ctx.services.builds.getRunningStatuses(buildIds) + const result = await ctx.buildsRepository.getRunningStatuses(buildIds) + if (!result.ok) { + throwTRPCErrorFromRepoError(result.error) + } + + return result.data }), - buildDetails: protectedTeamProcedure + buildDetails: buildsRepositoryProcedure .input( z.object({ templateId: z.string(), @@ -52,7 +75,11 @@ export const buildsRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const { buildId, templateId } = input - const buildInfo = await ctx.services.builds.getBuildInfo(buildId) + const buildInfoResult = await ctx.buildsRepository.getBuildInfo(buildId) + if (!buildInfoResult.ok) { + throwTRPCErrorFromRepoError(buildInfoResult.error) + } + const buildInfo = buildInfoResult.data const result: BuildDetailsModel = { templateNames: buildInfo.names, @@ -67,7 +94,7 @@ export const buildsRouter = createTRPCRouter({ return result }), - buildLogsBackwardsReversed: protectedTeamProcedure + buildLogsBackwardsReversed: buildsRepositoryProcedure .input( z.object({ templateId: z.string(), @@ -85,11 +112,15 @@ export const buildsRouter = createTRPCRouter({ const direction = 'backward' const limit = 100 - const buildLogs = await ctx.services.builds.getInfraBuildLogs( + const buildLogsResult = await ctx.buildsRepository.getInfraBuildLogs( templateId, buildId, { cursor, limit, direction, level } ) + if (!buildLogsResult.ok) { + throwTRPCErrorFromRepoError(buildLogsResult.error) + } + const buildLogs = buildLogsResult.data const logs: BuildLogModel[] = buildLogs.logs .map((log) => ({ @@ -111,7 +142,7 @@ export const buildsRouter = createTRPCRouter({ return result }), - buildLogsForward: protectedTeamProcedure + buildLogsForward: buildsRepositoryProcedure .input( z.object({ templateId: z.string(), @@ -129,11 +160,15 @@ export const buildsRouter = createTRPCRouter({ const direction = 'forward' const limit = 100 - const buildLogs = await ctx.services.builds.getInfraBuildLogs( + const buildLogsResult = await ctx.buildsRepository.getInfraBuildLogs( templateId, buildId, { cursor, limit, direction, level } ) + if (!buildLogsResult.ok) { + throwTRPCErrorFromRepoError(buildLogsResult.error) + } + const buildLogs = buildLogsResult.data const logs: BuildLogModel[] = buildLogs.logs.map( (log: { diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index fde605a5c..9c9b33696 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -1,5 +1,7 @@ import { millisecondsInDay } from 'date-fns/constants' import { z } from 'zod' +import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { deriveSandboxLifecycleFromEvents, mapApiSandboxRecordToModel, @@ -9,15 +11,25 @@ import { type SandboxLogModel, type SandboxLogsModel, } from '@/core/domains/sandboxes/models' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' import { SandboxIdSchema } from '@/lib/schemas/api' +const sandboxRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository( + createSandboxesRepository, + (sandboxesRepository) => ({ + sandboxesRepository, + }) + ) +) + export const sandboxRouter = createTRPCRouter({ // QUERIES - details: protectedTeamProcedure + details: sandboxRepositoryProcedure .input( z.object({ sandboxId: SandboxIdSchema, @@ -27,16 +39,24 @@ export const sandboxRouter = createTRPCRouter({ const { sandboxId } = input const detailsResult = - await ctx.services.sandboxes.getSandboxDetails(sandboxId) + await ctx.sandboxesRepository.getSandboxDetails(sandboxId) + if (!detailsResult.ok) { + throwTRPCErrorFromRepoError(detailsResult.error) + } const mappedDetails: SandboxDetailsModel = - detailsResult.source === 'infra' - ? mapInfraSandboxDetailsToModel(detailsResult.details) - : mapApiSandboxRecordToModel(detailsResult.details) - - const lifecycleEvents = - await ctx.services.sandboxes.getSandboxLifecycleEvents(sandboxId) - const derivedLifecycle = deriveSandboxLifecycleFromEvents(lifecycleEvents) + detailsResult.data.source === 'infra' + ? mapInfraSandboxDetailsToModel(detailsResult.data.details) + : mapApiSandboxRecordToModel(detailsResult.data.details) + + const lifecycleEventsResult = + await ctx.sandboxesRepository.getSandboxLifecycleEvents(sandboxId) + if (!lifecycleEventsResult.ok) { + throwTRPCErrorFromRepoError(lifecycleEventsResult.error) + } + const derivedLifecycle = deriveSandboxLifecycleFromEvents( + lifecycleEventsResult.data + ) const fallbackPausedAt = mappedDetails.state === 'paused' ? mappedDetails.endAt : null const fallbackEndedAt = @@ -55,7 +75,7 @@ export const sandboxRouter = createTRPCRouter({ } }), - logsBackwardsReversed: protectedTeamProcedure + logsBackwardsReversed: sandboxRepositoryProcedure .input( z.object({ sandboxId: SandboxIdSchema, @@ -73,10 +93,14 @@ export const sandboxRouter = createTRPCRouter({ const direction = 'backward' const limit = 100 - const sandboxLogs = await ctx.services.sandboxes.getSandboxLogs( + const sandboxLogsResult = await ctx.sandboxesRepository.getSandboxLogs( sandboxId, { cursor, limit, direction, level, search } ) + if (!sandboxLogsResult.ok) { + throwTRPCErrorFromRepoError(sandboxLogsResult.error) + } + const sandboxLogs = sandboxLogsResult.data const logs: SandboxLogModel[] = sandboxLogs.logs .map(mapInfraSandboxLogToModel) @@ -94,7 +118,7 @@ export const sandboxRouter = createTRPCRouter({ return result }), - logsForward: protectedTeamProcedure + logsForward: sandboxRepositoryProcedure .input( z.object({ sandboxId: SandboxIdSchema, @@ -112,10 +136,14 @@ export const sandboxRouter = createTRPCRouter({ const direction = 'forward' const limit = 100 - const sandboxLogs = await ctx.services.sandboxes.getSandboxLogs( + const sandboxLogsResult = await ctx.sandboxesRepository.getSandboxLogs( sandboxId, { cursor, limit, direction, level, search } ) + if (!sandboxLogsResult.ok) { + throwTRPCErrorFromRepoError(sandboxLogsResult.error) + } + const sandboxLogs = sandboxLogsResult.data const logs: SandboxLogModel[] = sandboxLogs.logs.map( mapInfraSandboxLogToModel @@ -132,7 +160,7 @@ export const sandboxRouter = createTRPCRouter({ return result }), - resourceMetrics: protectedTeamProcedure + resourceMetrics: sandboxRepositoryProcedure .input( z .object({ @@ -161,13 +189,17 @@ export const sandboxRouter = createTRPCRouter({ const { sandboxId } = input const { startMs, endMs } = input - const metrics = await ctx.services.sandboxes.getSandboxMetrics( + const metricsResult = await ctx.sandboxesRepository.getSandboxMetrics( sandboxId, { startUnixMs: startMs, endUnixMs: endMs, } ) + if (!metricsResult.ok) { + throwTRPCErrorFromRepoError(metricsResult.error) + } + const metrics = metricsResult.data return metrics }), diff --git a/src/core/server/api/routers/sandboxes.ts b/src/core/server/api/routers/sandboxes.ts index a1ed31083..a7e5d1528 100644 --- a/src/core/server/api/routers/sandboxes.ts +++ b/src/core/server/api/routers/sandboxes.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' +import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' import { calculateTeamMetricsStep, MOCK_SANDBOXES_DATA, @@ -14,12 +15,23 @@ import { fillTeamMetricsWithZeros, transformMetricsToClientMetrics, } from '@/core/server/functions/sandboxes/utils' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' +const sandboxesRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository( + createSandboxesRepository, + (sandboxesRepository) => ({ + sandboxesRepository, + }) + ) +) + export const sandboxesRouter = createTRPCRouter({ // QUERIES - getSandboxes: protectedTeamProcedure.query(async ({ ctx }) => { + getSandboxes: sandboxesRepositoryProcedure.query(async ({ ctx }) => { if (USE_MOCK_DATA) { await new Promise((resolve) => setTimeout(resolve, 200)) @@ -30,14 +42,17 @@ export const sandboxesRouter = createTRPCRouter({ } } - const sandboxes = await ctx.services.sandboxes.listSandboxes() + const sandboxesResult = await ctx.sandboxesRepository.listSandboxes() + if (!sandboxesResult.ok) { + throwTRPCErrorFromRepoError(sandboxesResult.error) + } return { - sandboxes, + sandboxes: sandboxesResult.data, } }), - getSandboxesMetrics: protectedTeamProcedure + getSandboxesMetrics: sandboxesRepositoryProcedure .input( z.object({ sandboxIds: z.array(z.string()), @@ -52,8 +67,12 @@ export const sandboxesRouter = createTRPCRouter({ } } - const metricsData = - await ctx.services.sandboxes.getSandboxesMetrics(sandboxIds) + const metricsDataResult = + await ctx.sandboxesRepository.getSandboxesMetrics(sandboxIds) + if (!metricsDataResult.ok) { + throwTRPCErrorFromRepoError(metricsDataResult.error) + } + const metricsData = metricsDataResult.data const metrics = transformMetricsToClientMetrics(metricsData) return { @@ -61,7 +80,7 @@ export const sandboxesRouter = createTRPCRouter({ } }), - getTeamMetrics: protectedTeamProcedure + getTeamMetrics: sandboxesRepositoryProcedure .input(GetTeamMetricsSchema) .query(async ({ ctx, input }) => { const { startDate: startDateMs, endDate: endDateMs } = input @@ -91,10 +110,14 @@ export const sandboxesRouter = createTRPCRouter({ // the overfetch is accounted for when post-processing the data using fillTeamMetricsWithZeros const overfetchS = Math.ceil(stepMs / 1000) - const metricData = await ctx.services.sandboxes.getTeamMetricsRange( + const metricDataResult = await ctx.sandboxesRepository.getTeamMetricsRange( startS, endS + overfetchS ) + if (!metricDataResult.ok) { + throwTRPCErrorFromRepoError(metricDataResult.error) + } + const metricData = metricDataResult.data // transform timestamps from seconds to milliseconds const metrics = metricData.map( @@ -123,7 +146,7 @@ export const sandboxesRouter = createTRPCRouter({ } }), - getTeamMetricsMax: protectedTeamProcedure + getTeamMetricsMax: sandboxesRepositoryProcedure .input(GetTeamMetricsMaxSchema) .query(async ({ ctx, input }) => { const { startDate: startDateMs, endDate: endDateMs, metric } = input @@ -136,11 +159,15 @@ export const sandboxesRouter = createTRPCRouter({ const startS = Math.floor(startDateMs / 1000) const endS = Math.floor(endDateMs / 1000) - const maxMetric = await ctx.services.sandboxes.getTeamMetricsMax( + const maxMetricResult = await ctx.sandboxesRepository.getTeamMetricsMax( startS, endS, metric ) + if (!maxMetricResult.ok) { + throwTRPCErrorFromRepoError(maxMetricResult.error) + } + const maxMetric = maxMetricResult.data // since javascript timestamps are in milliseconds, we want to convert the timestamp back to milliseconds const timestampMs = maxMetric.timestampUnix * 1000 diff --git a/src/core/server/api/routers/support.ts b/src/core/server/api/routers/support.ts index 2ce1eee04..89e95a3f0 100644 --- a/src/core/server/api/routers/support.ts +++ b/src/core/server/api/routers/support.ts @@ -1,5 +1,8 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' +import { createSupportRepository } from '@/core/domains/support/repository.server' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' @@ -11,8 +14,14 @@ const fileSchema = z.object({ base64: z.string(), }) +const supportRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository(createSupportRepository, (supportRepository) => ({ + supportRepository, + })) +) + export const supportRouter = createTRPCRouter({ - contactSupport: protectedTeamProcedure + contactSupport: supportRepositoryProcedure .input( z.object({ description: z.string().min(1), @@ -35,16 +44,24 @@ export const supportRouter = createTRPCRouter({ }) } - const team = await ctx.services.support.getTeamSupportData() + const teamResult = await ctx.supportRepository.getTeamSupportData() + if (!teamResult.ok) { + throwTRPCErrorFromRepoError(teamResult.error) + } - return ctx.services.support.createSupportThread({ + const createResult = await ctx.supportRepository.createSupportThread({ description: input.description, files: input.files, teamId, - teamName: team.name, + teamName: teamResult.data.name, customerEmail: email, - accountOwnerEmail: team.email, - customerTier: team.tier, + accountOwnerEmail: teamResult.data.email, + customerTier: teamResult.data.tier, }) + if (!createResult.ok) { + throwTRPCErrorFromRepoError(createResult.error) + } + + return createResult.data }), }) diff --git a/src/core/server/api/routers/teams.ts b/src/core/server/api/routers/teams.ts index d7b6ee3ab..c57f7eedb 100644 --- a/src/core/server/api/routers/teams.ts +++ b/src/core/server/api/routers/teams.ts @@ -1,13 +1,21 @@ import z from 'zod' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { withAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { protectedProcedure } from '@/core/server/trpc/procedures' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +const teamsRepositoryProcedure = protectedProcedure.use( + withAuthedRequestRepository(createUserTeamsRepository, (teamsRepository) => ({ + teamsRepository, + })) +) + export const teamsRouter = { - getCurrentTeam: protectedProcedure + getCurrentTeam: teamsRepositoryProcedure .input(z.object({ teamIdOrSlug: TeamIdOrSlugSchema })) .query(async ({ ctx, input }) => { - const teamResult = await ctx.services.teams.getCurrentUserTeam( + const teamResult = await ctx.teamsRepository.getCurrentUserTeam( input.teamIdOrSlug ) diff --git a/src/core/server/api/routers/templates.ts b/src/core/server/api/routers/templates.ts index 2702fa18c..16475690e 100644 --- a/src/core/server/api/routers/templates.ts +++ b/src/core/server/api/routers/templates.ts @@ -1,5 +1,13 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' +import { + createDefaultTemplatesRepository, + createTemplatesRepository, +} from '@/core/domains/templates/repository.server' +import { + withAuthedRequestRepository, + withTeamAuthedRequestRepository, +} from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { @@ -7,24 +15,42 @@ import { protectedTeamProcedure, } from '@/core/server/trpc/procedures' +const templatesRepositoryProcedure = protectedProcedure.use( + withAuthedRequestRepository( + createDefaultTemplatesRepository, + (templatesRepository) => ({ + templatesRepository, + }) + ) +) + +const teamTemplatesRepositoryProcedure = protectedTeamProcedure.use( + withTeamAuthedRequestRepository( + createTemplatesRepository, + (templatesRepository) => ({ + templatesRepository, + }) + ) +) + export const templatesRouter = createTRPCRouter({ // QUERIES - getTemplates: protectedTeamProcedure.query(async ({ ctx }) => { - const result = await ctx.services.templates.getTeamTemplates() + getTemplates: teamTemplatesRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.templatesRepository.getTeamTemplates() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), - getDefaultTemplatesCached: protectedProcedure.query(async ({ ctx }) => { - const result = await ctx.services.templates.getDefaultTemplatesCached() + getDefaultTemplatesCached: templatesRepositoryProcedure.query(async ({ ctx }) => { + const result = await ctx.templatesRepository.getDefaultTemplatesCached() if (!result.ok) throwTRPCErrorFromRepoError(result.error) return result.data }), // MUTATIONS - deleteTemplate: protectedTeamProcedure + deleteTemplate: teamTemplatesRepositoryProcedure .input( z.object({ templateId: z.string(), @@ -33,7 +59,7 @@ export const templatesRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const { templateId } = input - const result = await ctx.services.templates.deleteTemplate(templateId) + const result = await ctx.templatesRepository.deleteTemplate(templateId) if (!result.ok) { if ( @@ -54,7 +80,7 @@ export const templatesRouter = createTRPCRouter({ return result.data }), - updateTemplate: protectedTeamProcedure + updateTemplate: teamTemplatesRepositoryProcedure .input( z.object({ templateId: z.string(), @@ -64,7 +90,7 @@ export const templatesRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const { templateId, public: isPublic } = input - const result = await ctx.services.templates.updateTemplateVisibility( + const result = await ctx.templatesRepository.updateTemplateVisibility( templateId, isPublic ) diff --git a/src/core/server/context/from-route.ts b/src/core/server/context/from-route.ts deleted file mode 100644 index 51865626e..000000000 --- a/src/core/server/context/from-route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createRequestContext } from './request-context' - -export function createRouteServices(input: { - accessToken: string - teamId?: string -}) { - return createRequestContext({ - accessToken: input.accessToken, - teamId: input.teamId, - }).services -} diff --git a/src/core/server/context/request-context.ts b/src/core/server/context/request-context.ts deleted file mode 100644 index 630782817..000000000 --- a/src/core/server/context/request-context.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { createBillingRepository } from '@/core/domains/billing/repository.server' -import { createBuildsRepository } from '@/core/domains/builds/repository.server' -import { createKeysRepository } from '@/core/domains/keys/repository.server' -import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' -import { createSupportRepository } from '@/core/domains/support/repository.server' -import { createTeamsRepository } from '@/core/domains/teams/repository.server' -import { createTemplatesRepository } from '@/core/domains/templates/repository.server' -import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' - -export interface RequestScope { - accessToken: string - teamId?: string -} - -function buildRequestServices(scope: RequestScope) { - const requireTeamScope = () => { - if (!scope.teamId) { - throw new Error('teamId is required in request scope') - } - - return { - accessToken: scope.accessToken, - teamId: scope.teamId, - } - } - - return { - teams: createTeamsRepository(scope), - get builds() { - return createBuildsRepository(requireTeamScope()) - }, - get sandboxes() { - return createSandboxesRepository(requireTeamScope()) - }, - get templates() { - return createTemplatesRepository(requireTeamScope()) - }, - get billing() { - return createBillingRepository(requireTeamScope()) - }, - support: createSupportRepository(scope), - get keys() { - return createKeysRepository(requireTeamScope()) - }, - get webhooks() { - return createWebhooksRepository(requireTeamScope()) - }, - } -} - -export type RequestContextServices = ReturnType - -export interface RequestContext { - scope: RequestScope - services: RequestContextServices -} - -export function createRequestContext(scope: RequestScope): RequestContext { - let services: RequestContextServices | undefined - - return { - scope, - get services() { - services ??= buildRequestServices(scope) - return services - }, - } -} diff --git a/src/core/server/functions/keys/get-api-keys.ts b/src/core/server/functions/keys/get-api-keys.ts index 3c6c17dc1..dc857cbdb 100644 --- a/src/core/server/functions/keys/get-api-keys.ts +++ b/src/core/server/functions/keys/get-api-keys.ts @@ -1,9 +1,11 @@ -import 'server-only' - import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { createKeysRepository } from '@/core/domains/keys/repository.server' +import { + authActionClient, + withTeamIdResolution, +} from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' @@ -23,7 +25,10 @@ export const getTeamApiKeys = authActionClient const { session, teamId } = ctx - const result = await ctx.services.keys.listTeamApiKeys() + const result = await createKeysRepository({ + accessToken: session.access_token, + teamId, + }).listTeamApiKeys() if (!result.ok) { const status = result.error.status diff --git a/src/core/server/functions/team/get-team-id-from-segment.ts b/src/core/server/functions/team/get-team-id-from-segment.ts index 77c5a74ff..c9dfffcd0 100644 --- a/src/core/server/functions/team/get-team-id-from-segment.ts +++ b/src/core/server/functions/team/get-team-id-from-segment.ts @@ -2,7 +2,7 @@ import 'server-only' import z from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { createTeamsRepository } from '@/core/domains/teams/repository.server' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' @@ -26,7 +26,7 @@ export const getTeamIdFromSegment = async ( return segment } - const resolvedTeam = await createTeamsRepository({ + const resolvedTeam = await createUserTeamsRepository({ accessToken, }).resolveTeamBySlug(segment, { tags: [CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)], diff --git a/src/core/server/functions/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts index 4fab0da80..5950febfc 100644 --- a/src/core/server/functions/team/get-team-limits.ts +++ b/src/core/server/functions/team/get-team-limits.ts @@ -2,8 +2,13 @@ import 'server-only' import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' +import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' export interface TeamLimits { @@ -26,18 +31,22 @@ const GetTeamLimitsSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, }) +const withTeamsRepository = withTeamAuthedRequestRepository( + createTeamsRepository, + (teamsRepository) => ({ teamsRepository }) +) + export const getTeamLimits = authActionClient .schema(GetTeamLimitsSchema) .metadata({ serverFunctionName: 'getTeamLimits' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ ctx }) => { if (USE_MOCK_DATA) { return MOCK_TIER_LIMITS } - const limitsResult = await ctx.services.teams.getTeamLimitsByIdOrSlug( - ctx.teamId - ) + const limitsResult = await ctx.teamsRepository.getTeamLimits() if (!limitsResult.ok) { return toActionErrorFromRepoError(limitsResult.error) diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts index 8ed29eb76..4c5e584fa 100644 --- a/src/core/server/functions/team/get-team-members.ts +++ b/src/core/server/functions/team/get-team-members.ts @@ -1,10 +1,20 @@ import 'server-only' import { z } from 'zod' +import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +const withTeamsRepository = withTeamAuthedRequestRepository( + createTeamsRepository, + (teamsRepository) => ({ teamsRepository }) +) + const GetTeamMembersSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, }) @@ -13,8 +23,9 @@ export const getTeamMembers = authActionClient .schema(GetTeamMembersSchema) .metadata({ serverFunctionName: 'getTeamMembers' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ ctx }) => { - const result = await ctx.services.teams.listTeamMembers() + const result = await ctx.teamsRepository.listTeamMembers() if (!result.ok) { return toActionErrorFromRepoError(result.error) } diff --git a/src/core/server/functions/team/get-team.ts b/src/core/server/functions/team/get-team.ts index 7d1b32086..39e13a7da 100644 --- a/src/core/server/functions/team/get-team.ts +++ b/src/core/server/functions/team/get-team.ts @@ -1,11 +1,21 @@ import 'server-cli-only' import { z } from 'zod' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { + authActionClient, + withAuthedRequestRepository, + withTeamIdResolution, +} from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' +const withTeamsRepository = withAuthedRequestRepository( + createUserTeamsRepository, + (teamsRepository) => ({ teamsRepository }) +) + const GetTeamSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, }) @@ -14,8 +24,9 @@ export const getTeam = authActionClient .schema(GetTeamSchema) .metadata({ serverFunctionName: 'getTeam' }) .use(withTeamIdResolution) + .use(withTeamsRepository) .action(async ({ ctx }) => { - const teamResult = await ctx.services.teams.getCurrentUserTeam(ctx.teamId) + const teamResult = await ctx.teamsRepository.getCurrentUserTeam(ctx.teamId) if (!teamResult.ok) { return toActionErrorFromRepoError(teamResult.error) @@ -26,8 +37,9 @@ export const getTeam = authActionClient export const getUserTeams = authActionClient .metadata({ serverFunctionName: 'getUserTeams' }) + .use(withTeamsRepository) .action(async ({ ctx }) => { - const teamsResult = await ctx.services.teams.listUserTeams() + const teamsResult = await ctx.teamsRepository.listUserTeams() if (!teamsResult.ok || teamsResult.data.length === 0) { return returnServerError('No teams found.') diff --git a/src/core/server/functions/team/resolve-user-team.ts b/src/core/server/functions/team/resolve-user-team.ts index 01bc8c2be..8f5596317 100644 --- a/src/core/server/functions/team/resolve-user-team.ts +++ b/src/core/server/functions/team/resolve-user-team.ts @@ -3,7 +3,7 @@ import 'server-only' import { cookies } from 'next/headers' import { COOKIE_KEYS } from '@/configs/cookies' import type { ResolvedTeam } from '@/core/domains/teams/models' -import { createTeamsRepository } from '@/core/domains/teams/repository.server' +import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' import { l } from '@/lib/clients/logger/logger' export async function resolveUserTeam( @@ -18,7 +18,7 @@ export async function resolveUserTeam( return { id: cookieTeamId, slug: cookieTeamSlug } } - const teamsResult = await createTeamsRepository({ + const teamsResult = await createUserTeamsRepository({ accessToken, }).listUserTeams() diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts index 53f4b0138..f9be4711a 100644 --- a/src/core/server/functions/usage/get-usage.ts +++ b/src/core/server/functions/usage/get-usage.ts @@ -3,7 +3,11 @@ import 'server-only' import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { createBillingRepository } from '@/core/domains/billing/repository.server' +import { + authActionClient, + withTeamIdResolution, +} from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' @@ -23,7 +27,11 @@ export const getUsage = authActionClient cacheLife('hours') cacheTag(CACHE_TAGS.TEAM_USAGE(teamId)) - const result = await ctx.services.billing.getUsage() + const result = await createBillingRepository({ + accessToken: ctx.session.access_token, + teamId, + }).getUsage() + if (!result.ok) { return returnServerError(result.error.message) } diff --git a/src/core/server/functions/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts index e394e687c..5b0a95b28 100644 --- a/src/core/server/functions/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -1,7 +1,12 @@ import 'server-only' import { z } from 'zod' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' +import { + authActionClient, + withTeamIdResolution, + withTeamAuthedRequestRepository, +} from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError } from '@/lib/utils/action' @@ -10,14 +15,20 @@ const GetWebhooksSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, }) +const withWebhooksRepository = withTeamAuthedRequestRepository( + createWebhooksRepository, + (webhooksRepository) => ({ webhooksRepository }) +) + export const getWebhooks = authActionClient .schema(GetWebhooksSchema) .metadata({ serverFunctionName: 'getWebhook' }) .use(withTeamIdResolution) + .use(withWebhooksRepository) .action(async ({ ctx }) => { const { session, teamId } = ctx - const result = await ctx.services.webhooks.listWebhooks() + const result = await ctx.webhooksRepository.listWebhooks() if (!result.ok) { const status = result.error.status diff --git a/src/core/server/trpc/init.ts b/src/core/server/trpc/init.ts index 8b9859ce0..016d01b65 100644 --- a/src/core/server/trpc/init.ts +++ b/src/core/server/trpc/init.ts @@ -2,7 +2,6 @@ import type { Session, User } from '@supabase/supabase-js' import { initTRPC } from '@trpc/server' import superjson from 'superjson' import { flattenError, ZodError } from 'zod' -import type { RequestContextServices } from '@/core/server/context/request-context' /** * TRPC Context Factory @@ -15,7 +14,6 @@ export const createTRPCContext = async (opts: { headers: Headers }) => { session: undefined as Session | undefined, user: undefined as User | undefined, teamId: undefined as string | undefined, - services: undefined as RequestContextServices | undefined, } } diff --git a/src/core/server/trpc/procedures.ts b/src/core/server/trpc/procedures.ts index 8e9d9d6e1..d9f015949 100644 --- a/src/core/server/trpc/procedures.ts +++ b/src/core/server/trpc/procedures.ts @@ -6,7 +6,6 @@ import { endTelemetryMiddleware, startTelemetryMiddleware, } from '@/core/server/api/middlewares/telemetry' -import { createRequestContext } from '@/core/server/context/request-context' import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' import { getTracer } from '@/lib/clients/tracer' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' @@ -99,10 +98,6 @@ export const protectedTeamProcedure = t.procedure ctx: { ...ctx, teamId, - services: createRequestContext({ - accessToken: ctx.session.access_token, - teamId, - }).services, }, }) } finally { diff --git a/src/core/shared/repository-scope.ts b/src/core/shared/repository-scope.ts new file mode 100644 index 000000000..03af7669e --- /dev/null +++ b/src/core/shared/repository-scope.ts @@ -0,0 +1,7 @@ +export interface RequestScope { + accessToken: string +} + +export interface TeamRequestScope extends RequestScope { + teamId: string +} From 92f77f9468fa3c6bc0d2846e07b34e06f377dd26 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 18 Mar 2026 14:00:21 -0700 Subject: [PATCH 06/37] fix: profile picture handling and teams prefetching --- spec/openapi.dashboard-api.yaml | 13 ++- src/app/dashboard/[teamIdOrSlug]/layout.tsx | 4 +- .../dashboard/[teamIdOrSlug]/team-gate.tsx | 17 +++- src/core/domains/support/repository.server.ts | 22 +++-- .../domains/teams/teams-repository.server.ts | 36 +++++--- .../teams/user-teams-repository.server.ts | 34 ++------ src/core/server/api/routers/teams.ts | 22 ++--- src/core/server/functions/team/get-team.ts | 49 ----------- .../shared/contracts/dashboard-api.types.ts | 5 +- src/core/shared/schemas.ts | 3 - src/features/dashboard/context.tsx | 19 ++-- .../dashboard/members/danger-zone.tsx | 86 ------------------- src/features/dashboard/sidebar/menu-teams.tsx | 57 +----------- 13 files changed, 94 insertions(+), 273 deletions(-) delete mode 100644 src/core/server/functions/team/get-team.ts delete mode 100644 src/core/shared/schemas.ts delete mode 100644 src/features/dashboard/members/danger-zone.tsx diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml index cac2b24e5..263b643dd 100644 --- a/spec/openapi.dashboard-api.yaml +++ b/spec/openapi.dashboard-api.yaml @@ -380,6 +380,7 @@ components: - slug - tier - email + - profilePictureUrl - isDefault - limits properties: @@ -394,6 +395,9 @@ components: type: string email: type: string + profilePictureUrl: + type: string + nullable: true isDefault: type: boolean limits: @@ -445,13 +449,15 @@ components: UpdateTeamRequest: type: object - required: - - name + minProperties: 1 properties: name: type: string minLength: 1 maxLength: 255 + profilePictureUrl: + type: string + nullable: true UpdateTeamResponse: type: object @@ -464,6 +470,9 @@ components: format: uuid name: type: string + profilePictureUrl: + type: string + nullable: true AddTeamMemberRequest: type: object diff --git a/src/app/dashboard/[teamIdOrSlug]/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/layout.tsx index c8e461bf2..3ce00093d 100644 --- a/src/app/dashboard/[teamIdOrSlug]/layout.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/layout.tsx @@ -9,7 +9,7 @@ import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import DashboardLayoutView from '@/features/dashboard/layouts/layout' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { HydrateClient, prefetch, prefetchAsync, trpc } from '@/trpc/server' +import { HydrateClient, prefetch, trpc } from '@/trpc/server' import { SidebarInset, SidebarProvider } from '@/ui/primitives/sidebar' export const metadata: Metadata = { @@ -44,7 +44,7 @@ export default async function DashboardLayout({ throw redirect(AUTH_URLS.SIGN_IN) } - prefetch(trpc.teams.getCurrentTeam.queryOptions({ teamIdOrSlug })) + prefetch(trpc.teams.list.queryOptions()) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx index 86dd7f6f7..73e407b26 100644 --- a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx @@ -20,12 +20,23 @@ interface DashboardTeamGateProps { function TeamContent({ teamIdOrSlug, user, children }: DashboardTeamGateProps) { const trpc = useTRPC() - const { data: team } = useSuspenseQuery( - trpc.teams.getCurrentTeam.queryOptions({ teamIdOrSlug }) + const { data: teams } = useSuspenseQuery(trpc.teams.list.queryOptions()) + + const team = teams.find( + (candidate) => + candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug ) + if (!team) { + throw new Error('Team not found or access denied') + } + return ( - + {children} ) diff --git a/src/core/domains/support/repository.server.ts b/src/core/domains/support/repository.server.ts index d300a7575..f25e06eed 100644 --- a/src/core/domains/support/repository.server.ts +++ b/src/core/domains/support/repository.server.ts @@ -127,23 +127,33 @@ export function createSupportRepository( ): SupportRepository { return { async getTeamSupportData() { - const teamResult = await createUserTeamsRepository({ + const teamsResult = await createUserTeamsRepository({ accessToken: scope.accessToken, - }).getCurrentUserTeam(scope.teamId) + }).listUserTeams() - if (!teamResult.ok) { + if (!teamsResult.ok) { l.error( { key: 'repositories:support:fetch_team_error', - error: teamResult.error, + error: teamsResult.error, team_id: scope.teamId, }, 'failed to fetch team data' ) - return err(teamResult.error) + return err(teamsResult.error) } - const team = teamResult.data + const team = teamsResult.data.find( + (candidate) => candidate.id === scope.teamId + ) + + if (!team) { + return err( + repoErrorFromHttp(403, 'Team not found or access denied', { + teamIdOrSlug: scope.teamId, + }) + ) + } return ok({ name: team.name, email: team.email, tier: team.tier }) }, diff --git a/src/core/domains/teams/teams-repository.server.ts b/src/core/domains/teams/teams-repository.server.ts index d4e9b2fbe..e49beee95 100644 --- a/src/core/domains/teams/teams-repository.server.ts +++ b/src/core/domains/teams/teams-repository.server.ts @@ -1,13 +1,13 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { components as DashboardComponents } from '@/core/shared/contracts/dashboard-api.types' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { api } from '@/lib/clients/api' import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import type { components as DashboardComponents } from '@/types/dashboard-api.types' -import type { ClientTeam, TeamLimits, TeamMember } from './models' +import type { TeamLimits, TeamMember } from './models' type ApiUserTeam = { id: string @@ -38,7 +38,7 @@ export interface TeamsRepository { removeTeamMember(userId: string): Promise> updateTeamProfilePictureUrl( profilePictureUrl: string - ): Promise> + ): Promise> } export function createTeamsRepository( @@ -194,21 +194,29 @@ export function createTeamsRepository( }, async updateTeamProfilePictureUrl( profilePictureUrl - ): Promise> { - const { data, error } = await deps.adminClient - .from('teams') - .update({ profile_picture_url: profilePictureUrl }) - .eq('id', scope.teamId) - .select() - .single() - - if (error || !data) { + ): Promise< + RepoResult + > { + const { data, error, response } = await deps.apiClient.PATCH( + '/teams/{teamId}', + { + params: { path: { teamId: scope.teamId } }, + headers: deps.authHeaders(scope.accessToken, scope.teamId), + body: { profilePictureUrl }, + } + ) + + if (!response.ok || error || !data) { return err( - repoErrorFromHttp(500, error?.message ?? 'Failed to update team') + repoErrorFromHttp( + response.status, + error?.message ?? 'Failed to update team profile picture', + error + ) ) } - return ok(data as ClientTeam) + return ok(data) }, } } diff --git a/src/core/domains/teams/user-teams-repository.server.ts b/src/core/domains/teams/user-teams-repository.server.ts index 657b3b3c6..78a46a675 100644 --- a/src/core/domains/teams/user-teams-repository.server.ts +++ b/src/core/domains/teams/user-teams-repository.server.ts @@ -1,5 +1,6 @@ import 'server-only' +import { secondsInDay } from 'date-fns/constants' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { RequestScope } from '@/core/shared/repository-scope' @@ -13,6 +14,7 @@ type ApiUserTeam = { slug: string tier: string email: string + profilePictureUrl: string | null isDefault: boolean limits: { concurrentSandboxes: number @@ -36,7 +38,7 @@ function mapApiTeamToClientTeam(apiTeam: ApiUserTeam): ClientTeam { blocked_reason: null, cluster_id: null, created_at: '', - profile_picture_url: null, + profile_picture_url: apiTeam.profilePictureUrl, } } @@ -49,7 +51,6 @@ export type UserTeamsRequestScope = RequestScope export interface UserTeamsRepository { listUserTeams(): Promise> - getCurrentUserTeam(teamIdOrSlug: string): Promise> resolveTeamBySlug( slug: string, next?: { tags?: string[] } @@ -91,30 +92,6 @@ export function createUserTeamsRepository( return ok(teamsResult.data.map(mapApiTeamToClientTeam)) }, - async getCurrentUserTeam( - teamIdOrSlug: string - ): Promise> { - const teamsResult = await listApiUserTeams() - - if (!teamsResult.ok) { - return teamsResult - } - - const team = teamsResult.data.find( - (candidate) => - candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug - ) - - if (!team) { - return err( - repoErrorFromHttp(403, 'Team not found or access denied', { - teamIdOrSlug, - }) - ) - } - - return ok(mapApiTeamToClientTeam(team)) - }, async resolveTeamBySlug( slug: string, next?: { tags?: string[] } @@ -124,7 +101,10 @@ export function createUserTeamsRepository( { params: { query: { slug } }, headers: deps.authHeaders(scope.accessToken), - next, + next: { + revalidate: secondsInDay, + ...next, + }, } ) diff --git a/src/core/server/api/routers/teams.ts b/src/core/server/api/routers/teams.ts index c57f7eedb..2f63cad50 100644 --- a/src/core/server/api/routers/teams.ts +++ b/src/core/server/api/routers/teams.ts @@ -1,9 +1,7 @@ -import z from 'zod' import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { withAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { protectedProcedure } from '@/core/server/trpc/procedures' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' const teamsRepositoryProcedure = protectedProcedure.use( withAuthedRequestRepository(createUserTeamsRepository, (teamsRepository) => ({ @@ -12,17 +10,13 @@ const teamsRepositoryProcedure = protectedProcedure.use( ) export const teamsRouter = { - getCurrentTeam: teamsRepositoryProcedure - .input(z.object({ teamIdOrSlug: TeamIdOrSlugSchema })) - .query(async ({ ctx, input }) => { - const teamResult = await ctx.teamsRepository.getCurrentUserTeam( - input.teamIdOrSlug - ) + list: teamsRepositoryProcedure.query(async ({ ctx }) => { + const teamsResult = await ctx.teamsRepository.listUserTeams() - if (!teamResult.ok) { - throwTRPCErrorFromRepoError(teamResult.error) - } + if (!teamsResult.ok) { + throwTRPCErrorFromRepoError(teamsResult.error) + } - return teamResult.data - }), + return teamsResult.data + }), } diff --git a/src/core/server/functions/team/get-team.ts b/src/core/server/functions/team/get-team.ts deleted file mode 100644 index 39e13a7da..000000000 --- a/src/core/server/functions/team/get-team.ts +++ /dev/null @@ -1,49 +0,0 @@ -import 'server-cli-only' - -import { z } from 'zod' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { - authActionClient, - withAuthedRequestRepository, - withTeamIdResolution, -} from '@/core/server/actions/client' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' - -const withTeamsRepository = withAuthedRequestRepository( - createUserTeamsRepository, - (teamsRepository) => ({ teamsRepository }) -) - -const GetTeamSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, -}) - -export const getTeam = authActionClient - .schema(GetTeamSchema) - .metadata({ serverFunctionName: 'getTeam' }) - .use(withTeamIdResolution) - .use(withTeamsRepository) - .action(async ({ ctx }) => { - const teamResult = await ctx.teamsRepository.getCurrentUserTeam(ctx.teamId) - - if (!teamResult.ok) { - return toActionErrorFromRepoError(teamResult.error) - } - - return teamResult.data - }) - -export const getUserTeams = authActionClient - .metadata({ serverFunctionName: 'getUserTeams' }) - .use(withTeamsRepository) - .action(async ({ ctx }) => { - const teamsResult = await ctx.teamsRepository.listUserTeams() - - if (!teamsResult.ok || teamsResult.data.length === 0) { - return returnServerError('No teams found.') - } - - return teamsResult.data - }) diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts index f1528f078..51224dde7 100644 --- a/src/core/shared/contracts/dashboard-api.types.ts +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -654,6 +654,7 @@ export interface components { slug: string; tier: string; email: string; + profilePictureUrl: string | null; isDefault: boolean; limits: components["schemas"]["UserTeamLimits"]; }; @@ -674,12 +675,14 @@ export interface components { members: components["schemas"]["TeamMember"][]; }; UpdateTeamRequest: { - name: string; + name?: string; + profilePictureUrl?: string | null; }; UpdateTeamResponse: { /** Format: uuid */ id: string; name: string; + profilePictureUrl?: string | null; }; AddTeamMemberRequest: { /** Format: email */ diff --git a/src/core/shared/schemas.ts b/src/core/shared/schemas.ts deleted file mode 100644 index 5a9678f2f..000000000 --- a/src/core/shared/schemas.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { z } from 'zod' - -export const NonEmptyStringSchema = z.string().trim().min(1) diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index d590777bb..918094e04 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -1,15 +1,13 @@ 'use client' import type { User } from '@supabase/supabase-js' -import { createContext, type ReactNode, useContext, useState } from 'react' +import { createContext, type ReactNode, useContext } from 'react' import type { ClientTeam } from '@/core/domains/teams/models' interface DashboardContextValue { team: ClientTeam + teams: ClientTeam[] user: User - - setTeam: (team: ClientTeam) => void - setUser: (user: User) => void } const DashboardContext = createContext( @@ -19,23 +17,20 @@ const DashboardContext = createContext( interface DashboardContextProviderProps { children: ReactNode initialTeam: ClientTeam + initialTeams: ClientTeam[] initialUser: User } export function DashboardContextProvider({ children, initialTeam, + initialTeams, initialUser, }: DashboardContextProviderProps) { - const [team, setTeam] = useState(initialTeam) - const [user, setUser] = useState(initialUser) - const value = { - team, - user, - - setTeam, - setUser, + team: initialTeam, + teams: initialTeams, + user: initialUser, } return ( diff --git a/src/features/dashboard/members/danger-zone.tsx b/src/features/dashboard/members/danger-zone.tsx deleted file mode 100644 index 4036a8af5..000000000 --- a/src/features/dashboard/members/danger-zone.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { getTeam } from '@/core/server/functions/team/get-team' -import { AlertDialog } from '@/ui/alert-dialog' -import ErrorBoundary from '@/ui/error' -import { Button } from '@/ui/primitives/button' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/ui/primitives/card' - -interface DangerZoneProps { - teamId: string -} - -export function DangerZone({ teamId }: DangerZoneProps) { - return ( - - - Danger Zone - - Actions here can't be undone. Please proceed with caution. - - - - - - - ) -} - -async function DangerZoneContent({ teamId }: { teamId: string }) { - const res = await getTeam({ teamIdOrSlug: teamId }) - - if (!res?.data || res.serverError || res.validationErrors) { - return ( - - ) - } - - const team = res.data - - return ( - <> -
-
-

Leave Team

-

- Remove yourself from this Team -

-
- - {}} - trigger={ - - } - /> -
- -
-
-

Delete Team

-

- Permanently delete this team and all of its data -

-
- -
- - ) -} diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index 7d1f19550..dcea352be 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -1,10 +1,8 @@ import Link from 'next/link' import { usePathname, useSearchParams } from 'next/navigation' import { useCallback } from 'react' -import useSWR from 'swr' -import type { UserTeamsResponse } from '@/app/api/teams/user/types' -import { useTeamCookieManager } from '@/lib/hooks/use-team' import type { ClientTeam } from '@/core/domains/teams/models' +import { useTeamCookieManager } from '@/lib/hooks/use-team' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { DropdownMenuItem, @@ -12,7 +10,6 @@ import { DropdownMenuRadioGroup, DropdownMenuRadioItem, } from '@/ui/primitives/dropdown-menu' -import { Skeleton } from '@/ui/primitives/skeleton' import { useDashboard } from '../context' const PRESERVED_SEARCH_PARAMS = ['tab'] as const @@ -21,37 +18,10 @@ export default function DashboardSidebarMenuTeams() { const pathname = usePathname() const searchParams = useSearchParams() - const { user, team: selectedTeam } = useDashboard() + const { user, team: selectedTeam, teams } = useDashboard() useTeamCookieManager() - const { data: teams, isLoading } = useSWR( - ['/api/teams/user', user?.id], - async ([url, userId]: [string, string | undefined]) => { - if (!userId) { - return null - } - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - throw new Error(`Failed to fetch teams: ${response.status}`) - } - - const { teams } = (await response.json()) as UserTeamsResponse - - return teams - }, - { - keepPreviousData: true, - } - ) - const getNextUrl = useCallback( (team: ClientTeam) => { const splitPath = pathname.split('/') @@ -73,33 +43,12 @@ export default function DashboardSidebarMenuTeams() { [pathname, searchParams] ) - if (isLoading) { - return ( - <> - {user?.email && ( - - - - )} - {[1, 2].map((i) => ( -
- - -
- ))} - - ) - } - return ( {user?.email && ( {user.email} )} - {teams && teams.length > 0 ? ( + {teams.length > 0 ? ( teams.map((team) => ( From 8d8223ec19103aee8c562354a7ac21ebe6c1e82e Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 18 Mar 2026 14:02:10 -0700 Subject: [PATCH 07/37] chore: format --- src/__test__/unit/chart-utils.test.ts | 2 +- .../unit/fill-metrics-with-zeros.test.ts | 2 +- src/app/api/auth/verify-otp/route.ts | 5 +- src/app/api/teams/[teamId]/metrics/types.ts | 2 +- src/configs/mock-data.ts | 8 +- src/core/domains/auth/repository.server.ts | 4 +- src/core/domains/billing/repository.server.ts | 6 +- src/core/domains/builds/repository.server.ts | 14 +- .../domains/sandboxes/repository.server.ts | 5 +- src/core/server/actions/auth-actions.ts | 2 +- src/core/server/actions/client.ts | 10 +- src/core/server/actions/key-actions.ts | 2 +- src/core/server/actions/sandbox-actions.ts | 5 +- src/core/server/actions/team-actions.ts | 8 +- src/core/server/actions/webhooks-actions.ts | 2 +- src/core/server/api/routers/billing.ts | 15 +- src/core/server/api/routers/builds.ts | 11 +- src/core/server/api/routers/sandbox.ts | 6 +- src/core/server/api/routers/sandboxes.ts | 17 +- src/core/server/api/routers/support.ts | 11 +- src/core/server/api/routers/templates.ts | 16 +- .../sandboxes/get-team-metrics-core.ts | 2 +- .../sandboxes/get-team-metrics-max.ts | 5 +- .../functions/sandboxes/get-team-metrics.ts | 5 +- .../server/functions/team/get-team-limits.ts | 4 +- .../server/functions/team/get-team-members.ts | 4 +- .../server/functions/webhooks/get-webhooks.ts | 2 +- src/core/shared/contracts/argus-api.types.ts | 936 +-- .../shared/contracts/dashboard-api.types.ts | 1578 ++-- src/core/shared/contracts/infra-api.types.ts | 6417 +++++++++-------- src/features/dashboard/billing/addons.tsx | 2 +- .../dashboard/billing/select-plan.tsx | 2 +- src/features/dashboard/billing/types.ts | 2 +- src/features/dashboard/billing/utils.ts | 2 +- src/features/dashboard/limits/alert-card.tsx | 2 +- src/features/dashboard/limits/limit-card.tsx | 2 +- .../charts/team-metrics-chart/utils.ts | 2 +- .../dashboard/usage/usage-charts-context.tsx | 2 +- src/lib/hooks/use-team.ts | 2 +- src/lib/utils/rewrites.ts | 2 +- src/lib/utils/server.ts | 2 +- tsconfig.json | 12 +- 42 files changed, 4591 insertions(+), 4549 deletions(-) diff --git a/src/__test__/unit/chart-utils.test.ts b/src/__test__/unit/chart-utils.test.ts index 6c68a58fc..29fb54b55 100644 --- a/src/__test__/unit/chart-utils.test.ts +++ b/src/__test__/unit/chart-utils.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' +import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' import { transformMetrics } from '@/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils' import { calculateAxisMax } from '@/lib/utils/chart' -import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' describe('team-metrics-chart-utils', () => { describe('calculateYAxisMax', () => { diff --git a/src/__test__/unit/fill-metrics-with-zeros.test.ts b/src/__test__/unit/fill-metrics-with-zeros.test.ts index ce8bfcbd9..4ae91dfec 100644 --- a/src/__test__/unit/fill-metrics-with-zeros.test.ts +++ b/src/__test__/unit/fill-metrics-with-zeros.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' +import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' describe('fillTeamMetricsWithZeros', () => { describe('Empty data handling', () => { diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 25d66fd5a..0f15ddc76 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -111,7 +111,10 @@ export async function POST(request: NextRequest) { const verifyResult = await authRepository.verifyOtp(token_hash, type) if (!verifyResult.ok) { - const errorRedirectUrl = buildErrorRedirectUrl(origin, verifyResult.error.message) + const errorRedirectUrl = buildErrorRedirectUrl( + origin, + verifyResult.error.message + ) return NextResponse.json({ redirectUrl: errorRedirectUrl }) } diff --git a/src/app/api/teams/[teamId]/metrics/types.ts b/src/app/api/teams/[teamId]/metrics/types.ts index 5a7e8d174..3e354c5ad 100644 --- a/src/app/api/teams/[teamId]/metrics/types.ts +++ b/src/app/api/teams/[teamId]/metrics/types.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' +import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' export const TeamMetricsRequestSchema = z .object({ diff --git a/src/configs/mock-data.ts b/src/configs/mock-data.ts index b0fc95ebd..5b3c097c4 100644 --- a/src/configs/mock-data.ts +++ b/src/configs/mock-data.ts @@ -1,16 +1,16 @@ import { addHours, subHours } from 'date-fns' import { nanoid } from 'nanoid' import type { MetricsResponse } from '@/app/api/teams/[teamId]/sandboxes/metrics/types' +import type { + ClientSandboxesMetrics, + ClientTeamMetrics, +} from '@/core/domains/sandboxes/models.client' import type { DefaultTemplate, Sandbox, Sandboxes, Template, } from '@/types/api.types' -import type { - ClientSandboxesMetrics, - ClientTeamMetrics, -} from '@/core/domains/sandboxes/models.client' const DEFAULT_TEMPLATES: DefaultTemplate[] = [ { diff --git a/src/core/domains/auth/repository.server.ts b/src/core/domains/auth/repository.server.ts index 9f59b9289..b70edd42a 100644 --- a/src/core/domains/auth/repository.server.ts +++ b/src/core/domains/auth/repository.server.ts @@ -63,7 +63,9 @@ export function createAuthRepository(deps: AuthRepositoryDeps): AuthRepository { } if (!data.user) { - return err(repoErrorFromHttp(500, 'Verification failed. Please try again.')) + return err( + repoErrorFromHttp(500, 'Verification failed. Please try again.') + ) } return ok({ diff --git a/src/core/domains/billing/repository.server.ts b/src/core/domains/billing/repository.server.ts index d9f68216f..31d389baf 100644 --- a/src/core/domains/billing/repository.server.ts +++ b/src/core/domains/billing/repository.server.ts @@ -1,9 +1,6 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { repoErrorFromHttp } from '@/core/shared/errors' -import type { TeamRequestScope } from '@/core/shared/repository-scope' -import { err, ok, type RepoResult } from '@/core/shared/result' import type { AddOnOrderConfirmResponse, AddOnOrderCreateResponse, @@ -14,6 +11,9 @@ import type { TeamItems, UsageResponse, } from '@/core/domains/billing/models' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' type BillingRepositoryDeps = { billingApiUrl: string diff --git a/src/core/domains/builds/repository.server.ts b/src/core/domains/builds/repository.server.ts index c7db9d607..7fca3b8a8 100644 --- a/src/core/domains/builds/repository.server.ts +++ b/src/core/domains/builds/repository.server.ts @@ -1,15 +1,15 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { repoErrorFromHttp } from '@/core/shared/errors' -import type { TeamRequestScope } from '@/core/shared/repository-scope' -import { err, ok, type RepoResult } from '@/core/shared/result' import type { BuildStatus, ListedBuildModel, RunningBuildStatusModel, } from '@/core/domains/builds/models' import { l } from '@/core/shared/clients/logger/logger' +import { repoErrorFromHttp } from '@/core/shared/errors' +import type { TeamRequestScope } from '@/core/shared/repository-scope' +import { err, ok, type RepoResult } from '@/core/shared/result' import { INITIAL_BUILD_STATUSES } from '@/features/dashboard/templates/builds/constants' import { api, infra } from '@/lib/clients/api' import type { components as InfraComponents } from '@/types/infra-api.types' @@ -40,7 +40,9 @@ export interface BuildsRepository { templateId: string, buildId: string, options?: GetInfraBuildLogsOptions - ): Promise> + ): Promise< + RepoResult + > } const LIST_BUILDS_DEFAULT_LIMIT = 50 @@ -191,7 +193,9 @@ export function createBuildsRepository( (result.data?.buildStatuses ?? []).map((row) => ({ id: row.id, status: row.status, - finishedAt: row.finishedAt ? new Date(row.finishedAt).getTime() : null, + finishedAt: row.finishedAt + ? new Date(row.finishedAt).getTime() + : null, statusMessage: row.statusMessage, })) ) diff --git a/src/core/domains/sandboxes/repository.server.ts b/src/core/domains/sandboxes/repository.server.ts index 10b1f3ac6..6ff479ea0 100644 --- a/src/core/domains/sandboxes/repository.server.ts +++ b/src/core/domains/sandboxes/repository.server.ts @@ -1,10 +1,10 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { SandboxEventModel } from '@/core/domains/sandboxes/models' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import type { SandboxEventModel } from '@/core/domains/sandboxes/models' import { api, infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import type { @@ -75,7 +75,8 @@ export interface SandboxesRepository { ): Promise> } -const SANDBOX_NOT_FOUND_MESSAGE = "Sandbox not found or you don't have access to it" +const SANDBOX_NOT_FOUND_MESSAGE = + "Sandbox not found or you don't have access to it" const SANDBOX_EVENTS_PAGE_SIZE = 100 const SANDBOX_EVENTS_MAX_PAGES = 50 const SANDBOX_LIFECYCLE_EVENT_PREFIX = 'sandbox.lifecycle.' diff --git a/src/core/server/actions/auth-actions.ts b/src/core/server/actions/auth-actions.ts index 17f0db451..e30f34b55 100644 --- a/src/core/server/actions/auth-actions.ts +++ b/src/core/server/actions/auth-actions.ts @@ -7,6 +7,7 @@ import { z } from 'zod' import { CAPTCHA_REQUIRED_SERVER } from '@/configs/flags' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { USER_MESSAGES } from '@/configs/user-messages' +import { actionClient } from '@/core/server/actions/client' import { forgotPasswordSchema, signInSchema, @@ -17,7 +18,6 @@ import { validateEmail, } from '@/core/server/functions/auth/validate-email' import { verifyTurnstileToken } from '@/lib/captcha/turnstile' -import { actionClient } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { createClient } from '@/lib/clients/supabase/server' import { relativeUrlSchema } from '@/lib/schemas/url' diff --git a/src/core/server/actions/client.ts b/src/core/server/actions/client.ts index ad6e80656..136da7fc2 100644 --- a/src/core/server/actions/client.ts +++ b/src/core/server/actions/client.ts @@ -6,15 +6,15 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' -import type { - RequestScope, - TeamRequestScope, -} from '@/core/shared/repository-scope' import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' -import { UnauthenticatedError, UnknownError } from '@/core/shared/errors' import { l } from '@/core/shared/clients/logger/logger' import { createClient } from '@/core/shared/clients/supabase/server' import { getTracer } from '@/core/shared/clients/tracer' +import { UnauthenticatedError, UnknownError } from '@/core/shared/errors' +import type { + RequestScope, + TeamRequestScope, +} from '@/core/shared/repository-scope' import { ActionError, flattenClientInputValue } from './utils' type SupabaseServerClient = Awaited> diff --git a/src/core/server/actions/key-actions.ts b/src/core/server/actions/key-actions.ts index 0a378e806..a9072091b 100644 --- a/src/core/server/actions/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -6,8 +6,8 @@ import { CACHE_TAGS } from '@/configs/cache' import { createKeysRepository } from '@/core/domains/keys/repository.server' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/server/actions/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts index 1c6fed5f9..1bd283968 100644 --- a/src/core/server/actions/sandbox-actions.ts +++ b/src/core/server/actions/sandbox-actions.ts @@ -4,7 +4,10 @@ import { updateTag } from 'next/cache' import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' +import { + authActionClient, + withTeamIdResolution, +} from '@/core/server/actions/client' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/server/actions/team-actions.ts b/src/core/server/actions/team-actions.ts index 3a4b8e00e..33e08057b 100644 --- a/src/core/server/actions/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -8,22 +8,22 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { zfd } from 'zod-form-data' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +import type { CreateTeamsResponse } from '@/core/domains/billing/models' import { CreateTeamSchema, UpdateTeamNameSchema, } from '@/core/domains/teams/schemas' -import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { l } from '@/lib/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { handleDefaultInfraError, returnServerError } from '@/lib/utils/action' -import type { CreateTeamsResponse } from '@/core/domains/billing/models' const withTeamsRepository = withTeamAuthedRequestRepository( createTeamsRepository, diff --git a/src/core/server/actions/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts index 14c69115f..8c788af1b 100644 --- a/src/core/server/actions/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -6,8 +6,8 @@ import { COOKIE_KEYS } from '@/configs/cookies' import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' import { DeleteWebhookSchema, diff --git a/src/core/server/api/routers/billing.ts b/src/core/server/api/routers/billing.ts index 46158e771..3bceb7cba 100644 --- a/src/core/server/api/routers/billing.ts +++ b/src/core/server/api/routers/billing.ts @@ -3,8 +3,8 @@ import { headers } from 'next/headers' import { z } from 'zod' import { createBillingRepository } from '@/core/domains/billing/repository.server' import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' -import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' import { @@ -17,9 +17,12 @@ function limitTypeToKey(type: 'limit' | 'alert') { } const billingRepositoryProcedure = protectedTeamProcedure.use( - withTeamAuthedRequestRepository(createBillingRepository, (billingRepository) => ({ - billingRepository, - })) + withTeamAuthedRequestRepository( + createBillingRepository, + (billingRepository) => ({ + billingRepository, + }) + ) ) const billingAndTeamsRepositoryProcedure = billingRepositoryProcedure.use( @@ -100,7 +103,9 @@ export const billingRouter = createTRPCRouter({ .input(z.object({ type: z.enum(['limit', 'alert']) })) .mutation(async ({ ctx, input }) => { const { type } = input - const result = await ctx.billingRepository.clearLimit(limitTypeToKey(type)) + const result = await ctx.billingRepository.clearLimit( + limitTypeToKey(type) + ) if (!result.ok) throwTRPCErrorFromRepoError(result.error) }), diff --git a/src/core/server/api/routers/builds.ts b/src/core/server/api/routers/builds.ts index be1a7a56d..a10b1cc38 100644 --- a/src/core/server/api/routers/builds.ts +++ b/src/core/server/api/routers/builds.ts @@ -6,16 +6,19 @@ import { BuildStatusSchema, } from '@/core/domains/builds/models' import { createBuildsRepository } from '@/core/domains/builds/repository.server' -import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' import { LOG_RETENTION_MS } from '@/features/dashboard/templates/builds/constants' const buildsRepositoryProcedure = protectedTeamProcedure.use( - withTeamAuthedRequestRepository(createBuildsRepository, (buildsRepository) => ({ - buildsRepository, - })) + withTeamAuthedRequestRepository( + createBuildsRepository, + (buildsRepository) => ({ + buildsRepository, + }) + ) ) export const buildsRouter = createTRPCRouter({ diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index 9c9b33696..a93ef2708 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -1,7 +1,5 @@ import { millisecondsInDay } from 'date-fns/constants' import { z } from 'zod' -import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' -import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { deriveSandboxLifecycleFromEvents, mapApiSandboxRecordToModel, @@ -11,7 +9,9 @@ import { type SandboxLogModel, type SandboxLogsModel, } from '@/core/domains/sandboxes/models' +import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' @@ -21,7 +21,7 @@ const sandboxRepositoryProcedure = protectedTeamProcedure.use( withTeamAuthedRequestRepository( createSandboxesRepository, (sandboxesRepository) => ({ - sandboxesRepository, + sandboxesRepository, }) ) ) diff --git a/src/core/server/api/routers/sandboxes.ts b/src/core/server/api/routers/sandboxes.ts index a7e5d1528..861eb523a 100644 --- a/src/core/server/api/routers/sandboxes.ts +++ b/src/core/server/api/routers/sandboxes.ts @@ -1,22 +1,22 @@ import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' -import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' import { calculateTeamMetricsStep, MOCK_SANDBOXES_DATA, MOCK_TEAM_METRICS_DATA, MOCK_TEAM_METRICS_MAX_DATA, } from '@/configs/mock-data' +import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' import { GetTeamMetricsMaxSchema, GetTeamMetricsSchema, } from '@/core/domains/sandboxes/schemas' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { fillTeamMetricsWithZeros, transformMetricsToClientMetrics, } from '@/core/server/functions/sandboxes/utils' -import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' -import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' @@ -24,7 +24,7 @@ const sandboxesRepositoryProcedure = protectedTeamProcedure.use( withTeamAuthedRequestRepository( createSandboxesRepository, (sandboxesRepository) => ({ - sandboxesRepository, + sandboxesRepository, }) ) ) @@ -110,10 +110,11 @@ export const sandboxesRouter = createTRPCRouter({ // the overfetch is accounted for when post-processing the data using fillTeamMetricsWithZeros const overfetchS = Math.ceil(stepMs / 1000) - const metricDataResult = await ctx.sandboxesRepository.getTeamMetricsRange( - startS, - endS + overfetchS - ) + const metricDataResult = + await ctx.sandboxesRepository.getTeamMetricsRange( + startS, + endS + overfetchS + ) if (!metricDataResult.ok) { throwTRPCErrorFromRepoError(metricDataResult.error) } diff --git a/src/core/server/api/routers/support.ts b/src/core/server/api/routers/support.ts index 89e95a3f0..4b94d72a3 100644 --- a/src/core/server/api/routers/support.ts +++ b/src/core/server/api/routers/support.ts @@ -1,8 +1,8 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' import { createSupportRepository } from '@/core/domains/support/repository.server' -import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' +import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' @@ -15,9 +15,12 @@ const fileSchema = z.object({ }) const supportRepositoryProcedure = protectedTeamProcedure.use( - withTeamAuthedRequestRepository(createSupportRepository, (supportRepository) => ({ - supportRepository, - })) + withTeamAuthedRequestRepository( + createSupportRepository, + (supportRepository) => ({ + supportRepository, + }) + ) ) export const supportRouter = createTRPCRouter({ diff --git a/src/core/server/api/routers/templates.ts b/src/core/server/api/routers/templates.ts index 16475690e..6d97a4864 100644 --- a/src/core/server/api/routers/templates.ts +++ b/src/core/server/api/routers/templates.ts @@ -4,11 +4,11 @@ import { createDefaultTemplatesRepository, createTemplatesRepository, } from '@/core/domains/templates/repository.server' +import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withAuthedRequestRepository, withTeamAuthedRequestRepository, } from '@/core/server/api/middlewares/repository' -import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedProcedure, @@ -28,7 +28,7 @@ const teamTemplatesRepositoryProcedure = protectedTeamProcedure.use( withTeamAuthedRequestRepository( createTemplatesRepository, (templatesRepository) => ({ - templatesRepository, + templatesRepository, }) ) ) @@ -42,11 +42,13 @@ export const templatesRouter = createTRPCRouter({ return result.data }), - getDefaultTemplatesCached: templatesRepositoryProcedure.query(async ({ ctx }) => { - const result = await ctx.templatesRepository.getDefaultTemplatesCached() - if (!result.ok) throwTRPCErrorFromRepoError(result.error) - return result.data - }), + getDefaultTemplatesCached: templatesRepositoryProcedure.query( + async ({ ctx }) => { + const result = await ctx.templatesRepository.getDefaultTemplatesCached() + if (!result.ok) throwTRPCErrorFromRepoError(result.error) + return result.data + } + ), // MUTATIONS diff --git a/src/core/server/functions/sandboxes/get-team-metrics-core.ts b/src/core/server/functions/sandboxes/get-team-metrics-core.ts index 50b12684f..939bf4c88 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-core.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-core.ts @@ -7,11 +7,11 @@ import { calculateTeamMetricsStep, MOCK_TEAM_METRICS_DATA, } from '@/configs/mock-data' +import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { handleDefaultInfraError } from '@/lib/utils/action' -import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' interface GetTeamMetricsCoreParams { accessToken: string diff --git a/src/core/server/functions/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts index c624c54aa..d832c1bde 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-max.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-max.ts @@ -4,8 +4,11 @@ import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { USE_MOCK_DATA } from '@/configs/flags' import { MOCK_TEAM_METRICS_MAX_DATA } from '@/configs/mock-data' +import { + authActionClient, + withTeamIdResolution, +} from '@/core/server/actions/client' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/server/functions/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts index 3bbc5660b..5d2ea5607 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics.ts @@ -1,8 +1,11 @@ import 'server-only' import { z } from 'zod' +import { + authActionClient, + withTeamIdResolution, +} from '@/core/server/actions/client' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { authActionClient, withTeamIdResolution } from '@/core/server/actions/client' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' import { returnServerError } from '@/lib/utils/action' import { getTeamMetricsCore } from './get-team-metrics-core' diff --git a/src/core/server/functions/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts index 5950febfc..9327d0d60 100644 --- a/src/core/server/functions/team/get-team-limits.ts +++ b/src/core/server/functions/team/get-team-limits.ts @@ -3,12 +3,12 @@ import 'server-only' import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' -import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' export interface TeamLimits { diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts index 4c5e584fa..67273a708 100644 --- a/src/core/server/functions/team/get-team-members.ts +++ b/src/core/server/functions/team/get-team-members.ts @@ -2,12 +2,12 @@ import 'server-only' import { z } from 'zod' import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' -import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' +import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' const withTeamsRepository = withTeamAuthedRequestRepository( diff --git a/src/core/server/functions/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts index 5b0a95b28..a59311563 100644 --- a/src/core/server/functions/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -4,8 +4,8 @@ import { z } from 'zod' import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' import { authActionClient, - withTeamIdResolution, withTeamAuthedRequestRepository, + withTeamIdResolution, } from '@/core/server/actions/client' import { l } from '@/lib/clients/logger/logger' import { TeamIdOrSlugSchema } from '@/lib/schemas/team' diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index fb40ff39f..b9286818e 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -4,476 +4,476 @@ */ export interface paths { - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Health check */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Request was successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/sandboxes/{sandboxID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get sandbox events */ - get: { - parameters: { - query?: { - offset?: number; - limit?: number; - orderAsc?: boolean; - }; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox events */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxEvent"][]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/sandboxes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get all sandbox events for the team associated with the API key */ - get: { - parameters: { - query?: { - offset?: number; - limit?: number; - orderAsc?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox events */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxEvent"][]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/webhooks": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List registered webhooks. */ - get: operations["webhooksList"]; - put?: never; - /** @description Register events webhook. */ - post: operations["webhookCreate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/webhooks/{webhookID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get a registered webhook. */ - get: operations["webhookGet"]; - put?: never; - post?: never; - /** @description Delete a registered webhook. */ - delete: operations["webhookDelete"]; - options?: never; - head?: never; - /** @description Update a registered webhook configuration. */ - patch: operations["webhookUpdate"]; - trace?: never; - }; + '/health': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Health check */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Request was successful */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/events/sandboxes/{sandboxID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get sandbox events */ + get: { + parameters: { + query?: { + offset?: number + limit?: number + orderAsc?: boolean + } + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox events */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxEvent'][] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/events/sandboxes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get all sandbox events for the team associated with the API key */ + get: { + parameters: { + query?: { + offset?: number + limit?: number + orderAsc?: boolean + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox events */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxEvent'][] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/events/webhooks': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List registered webhooks. */ + get: operations['webhooksList'] + put?: never + /** @description Register events webhook. */ + post: operations['webhookCreate'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/events/webhooks/{webhookID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get a registered webhook. */ + get: operations['webhookGet'] + put?: never + post?: never + /** @description Delete a registered webhook. */ + delete: operations['webhookDelete'] + options?: never + head?: never + /** @description Update a registered webhook configuration. */ + patch: operations['webhookUpdate'] + trace?: never + } } -export type webhooks = Record; +export type webhooks = Record export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code - */ - code: number; - /** @description Error */ - message: string; - }; - /** @description Sandbox event */ - SandboxEvent: { - /** - * Format: uuid - * @description Event unique identifier - */ - id: string; - /** @description Event structure version */ - version: string; - /** @description Event name */ - type: string; - /** - * @deprecated - * @description Category of the event (e.g., 'lifecycle', 'process', etc.) - */ - eventCategory?: string; - /** - * @deprecated - * @description Label for the specific event type (e.g., 'sandbox_started', 'process_oom', etc.) - */ - eventLabel?: string; - /** @description Optional JSON data associated with the event */ - eventData?: Record | null; - /** - * Format: date-time - * @description Timestamp of the event - */ - timestamp: string; - /** - * Format: string - * @description Unique identifier for the sandbox - */ - sandboxId: string; - /** - * Format: string - * @description Unique identifier for the sandbox execution - */ - sandboxExecutionId: string; - /** - * Format: string - * @description Unique identifier for the sandbox template - */ - sandboxTemplateId: string; - /** - * Format: string - * @description Unique identifier for the sandbox build - */ - sandboxBuildId: string; - /** - * Format: uuid - * @description Team identifier associated with the sandbox - */ - sandboxTeamId: string; - }; - /** @description Configuration for registering new webhooks */ - WebhookCreate: { - name: string; - /** Format: uri */ - url: string; - events: string[]; - /** @default true */ - enabled: boolean; - /** @description Secret used to sign the webhook payloads */ - signatureSecret: string; - }; - /** @description Webhook creation response */ - WebhookCreation: { - /** @description Webhook unique identifier */ - id: string; - /** @description Webhook user friendly name */ - name: string; - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string; - /** @description Unique identifier for the team */ - teamId: string; - /** Format: uri */ - url: string; - enabled: boolean; - events: string[]; - }; - /** @description Webhook detail response */ - WebhookDetail: { - /** @description Webhook unique identifier */ - id: string; - /** @description Unique identifier for the team */ - teamId: string; - /** @description Webhook user friendly name */ - name: string; - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string; - /** Format: uri */ - url: string; - enabled: boolean; - events: string[]; - }; - /** @description Configuration for updating existing webhooks */ - WebhookConfiguration: { - enabled?: boolean; - /** @description Webhook user friendly name */ - name?: string; - /** Format: uri */ - url?: string; - events?: string[]; - /** @description Secret used to sign the webhook payloads */ - signatureSecret?: string; - }; - }; - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - }; - parameters: { - sandboxID: string; - webhookID: string; - }; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + Error: { + /** + * Format: int32 + * @description Error code + */ + code: number + /** @description Error */ + message: string + } + /** @description Sandbox event */ + SandboxEvent: { + /** + * Format: uuid + * @description Event unique identifier + */ + id: string + /** @description Event structure version */ + version: string + /** @description Event name */ + type: string + /** + * @deprecated + * @description Category of the event (e.g., 'lifecycle', 'process', etc.) + */ + eventCategory?: string + /** + * @deprecated + * @description Label for the specific event type (e.g., 'sandbox_started', 'process_oom', etc.) + */ + eventLabel?: string + /** @description Optional JSON data associated with the event */ + eventData?: Record | null + /** + * Format: date-time + * @description Timestamp of the event + */ + timestamp: string + /** + * Format: string + * @description Unique identifier for the sandbox + */ + sandboxId: string + /** + * Format: string + * @description Unique identifier for the sandbox execution + */ + sandboxExecutionId: string + /** + * Format: string + * @description Unique identifier for the sandbox template + */ + sandboxTemplateId: string + /** + * Format: string + * @description Unique identifier for the sandbox build + */ + sandboxBuildId: string + /** + * Format: uuid + * @description Team identifier associated with the sandbox + */ + sandboxTeamId: string + } + /** @description Configuration for registering new webhooks */ + WebhookCreate: { + name: string + /** Format: uri */ + url: string + events: string[] + /** @default true */ + enabled: boolean + /** @description Secret used to sign the webhook payloads */ + signatureSecret: string + } + /** @description Webhook creation response */ + WebhookCreation: { + /** @description Webhook unique identifier */ + id: string + /** @description Webhook user friendly name */ + name: string + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string + /** @description Unique identifier for the team */ + teamId: string + /** Format: uri */ + url: string + enabled: boolean + events: string[] + } + /** @description Webhook detail response */ + WebhookDetail: { + /** @description Webhook unique identifier */ + id: string + /** @description Unique identifier for the team */ + teamId: string + /** @description Webhook user friendly name */ + name: string + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string + /** Format: uri */ + url: string + enabled: boolean + events: string[] + } + /** @description Configuration for updating existing webhooks */ + WebhookConfiguration: { + enabled?: boolean + /** @description Webhook user friendly name */ + name?: string + /** Format: uri */ + url?: string + events?: string[] + /** @description Secret used to sign the webhook payloads */ + signatureSecret?: string + } + } + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + } + parameters: { + sandboxID: string + webhookID: string + } + requestBodies: never + headers: never + pathItems: never } -export type $defs = Record; +export type $defs = Record export interface operations { - webhooksList: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of registered webhooks. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["WebhookDetail"][]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - webhookCreate: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["WebhookCreate"]; - }; - }; - responses: { - /** @description Successfully created webhook. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["WebhookCreation"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - webhookGet: { - parameters: { - query?: never; - header?: never; - path: { - webhookID: components["parameters"]["webhookID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the webhook configuration. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["WebhookDetail"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - webhookDelete: { - parameters: { - query?: never; - header?: never; - path: { - webhookID: components["parameters"]["webhookID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully deleted webhook. */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - webhookUpdate: { - parameters: { - query?: never; - header?: never; - path: { - webhookID: components["parameters"]["webhookID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["WebhookConfiguration"]; - }; - }; - responses: { - /** @description Successfully updated webhook. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["WebhookDetail"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; + webhooksList: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description List of registered webhooks. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['WebhookDetail'][] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + webhookCreate: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['WebhookCreate'] + } + } + responses: { + /** @description Successfully created webhook. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['WebhookCreation'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + webhookGet: { + parameters: { + query?: never + header?: never + path: { + webhookID: components['parameters']['webhookID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the webhook configuration. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['WebhookDetail'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + webhookDelete: { + parameters: { + query?: never + header?: never + path: { + webhookID: components['parameters']['webhookID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully deleted webhook. */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + webhookUpdate: { + parameters: { + query?: never + header?: never + path: { + webhookID: components['parameters']['webhookID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['WebhookConfiguration'] + } + } + responses: { + /** @description Successfully updated webhook. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['WebhookDetail'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } } diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts index 51224dde7..6d8346fa5 100644 --- a/src/core/shared/contracts/dashboard-api.types.ts +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -4,795 +4,795 @@ */ export interface paths { - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Health check */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Health check successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List team builds */ - get: { - parameters: { - query?: { - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template?: components["parameters"]["build_id_or_template"]; - /** @description Comma-separated list of build statuses to include. */ - statuses?: components["parameters"]["build_statuses"]; - /** @description Maximum number of items to return per page. */ - limit?: components["parameters"]["builds_limit"]; - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - cursor?: components["parameters"]["builds_cursor"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned paginated builds. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildsListResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds/statuses": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get build statuses */ - get: { - parameters: { - query: { - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: components["parameters"]["build_ids"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned build statuses */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildsStatusesResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/builds/{build_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get build details */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the build. */ - build_id: components["parameters"]["build_id"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned build details. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BuildInfo"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/record": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get sandbox record */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the sandbox. */ - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned sandbox details. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxRecord"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List user teams - * @description Returns all teams the authenticated user belongs to, with limits and default flag. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned user teams. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserTeamsResponse"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/resolve": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Resolve team identity - * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. - */ - get: { - parameters: { - query: { - /** @description Team slug to resolve. */ - slug: components["parameters"]["teamSlug"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully resolved team. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamResolveResponse"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update team */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTeamRequest"]; - }; - }; - responses: { - /** @description Successfully updated team. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UpdateTeamResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - trace?: never; - }; - "/teams/{teamId}/members": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List team members */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned team members. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamMembersResponse"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** Add team member */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AddTeamMemberRequest"]; - }; - }; - responses: { - /** @description Successfully added team member. */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/members/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove team member */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Identifier of the team. */ - teamId: components["parameters"]["teamId"]; - /** @description Identifier of the user. */ - userId: components["parameters"]["userId"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully removed team member. */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/defaults": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List default templates - * @description Returns the list of default templates with their latest build info and aliases. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned default templates. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DefaultTemplatesResponse"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; + '/health': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Health check */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Health check successful */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['HealthResponse'] + } + } + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List team builds */ + get: { + parameters: { + query?: { + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template?: components['parameters']['build_id_or_template'] + /** @description Comma-separated list of build statuses to include. */ + statuses?: components['parameters']['build_statuses'] + /** @description Maximum number of items to return per page. */ + limit?: components['parameters']['builds_limit'] + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + cursor?: components['parameters']['builds_cursor'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned paginated builds. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildsListResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds/statuses': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get build statuses */ + get: { + parameters: { + query: { + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: components['parameters']['build_ids'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned build statuses */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildsStatusesResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/builds/{build_id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get build details */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the build. */ + build_id: components['parameters']['build_id'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned build details. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['BuildInfo'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/record': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get sandbox record */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the sandbox. */ + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned sandbox details. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxRecord'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * List user teams + * @description Returns all teams the authenticated user belongs to, with limits and default flag. + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned user teams. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UserTeamsResponse'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/resolve': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Resolve team identity + * @description Resolves a team slug or UUID to the team's identity, validating the user is a member. + */ + get: { + parameters: { + query: { + /** @description Team slug to resolve. */ + slug: components['parameters']['teamSlug'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully resolved team. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamResolveResponse'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** Update team */ + patch: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateTeamRequest'] + } + } + responses: { + /** @description Successfully updated team. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateTeamResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + trace?: never + } + '/teams/{teamId}/members': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List team members */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned team members. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamMembersResponse'] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + /** Add team member */ + post: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AddTeamMemberRequest'] + } + } + responses: { + /** @description Successfully added team member. */ + 201: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamId}/members/{userId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** Remove team member */ + delete: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamId: components['parameters']['teamId'] + /** @description Identifier of the user. */ + userId: components['parameters']['userId'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully removed team member. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/defaults': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * List default templates + * @description Returns the list of default templates with their latest build info and aliases. + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned default templates. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['DefaultTemplatesResponse'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } } -export type webhooks = Record; +export type webhooks = Record export interface components { - schemas: { - Error: { - /** - * Format: int32 - * @description Error code. - */ - code: number; - /** @description Error message. */ - message: string; - }; - /** - * @description Build status mapped for dashboard clients. - * @enum {string} - */ - BuildStatus: "building" | "failed" | "success"; - ListedBuild: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string; - /** @description Template alias when present, otherwise template ID. */ - template: string; - /** @description Identifier of the template. */ - templateId: string; - status: components["schemas"]["BuildStatus"]; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - }; - BuildsListResponse: { - data: components["schemas"]["ListedBuild"][]; - /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ - nextCursor: string | null; - }; - BuildStatusItem: { - /** - * Format: uuid - * @description Identifier of the build. - */ - id: string; - status: components["schemas"]["BuildStatus"]; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - }; - BuildsStatusesResponse: { - /** @description List of build statuses */ - buildStatuses: components["schemas"]["BuildStatusItem"][]; - }; - BuildInfo: { - /** @description Template names related to this build, if available. */ - names?: string[] | null; - /** - * Format: date-time - * @description Build creation timestamp in RFC3339 format. - */ - createdAt: string; - /** - * Format: date-time - * @description Build completion timestamp in RFC3339 format, if finished. - */ - finishedAt: string | null; - status: components["schemas"]["BuildStatus"]; - /** @description Failure message when status is `failed`, otherwise `null`. */ - statusMessage: string | null; - }; - /** - * Format: int64 - * @description CPU cores for the sandbox - */ - CPUCount: number; - /** - * Format: int64 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number; - /** - * Format: int64 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number; - SandboxRecord: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string; - /** @description Alias of the template */ - alias?: string; - /** @description Identifier of the sandbox */ - sandboxID: string; - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string; - /** - * Format: date-time - * @description Time when the sandbox was stopped - */ - stoppedAt?: string | null; - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - }; - HealthResponse: { - /** @description Human-readable health check result. */ - message: string; - }; - UserTeamLimits: { - /** Format: int64 */ - maxLengthHours: number; - /** Format: int32 */ - concurrentSandboxes: number; - /** Format: int32 */ - concurrentTemplateBuilds: number; - /** Format: int32 */ - maxVcpu: number; - /** Format: int32 */ - maxRamMb: number; - /** Format: int32 */ - diskMb: number; - }; - UserTeam: { - /** Format: uuid */ - id: string; - name: string; - slug: string; - tier: string; - email: string; - profilePictureUrl: string | null; - isDefault: boolean; - limits: components["schemas"]["UserTeamLimits"]; - }; - UserTeamsResponse: { - teams: components["schemas"]["UserTeam"][]; - }; - TeamMember: { - /** Format: uuid */ - id: string; - email: string; - isDefault: boolean; - /** Format: uuid */ - addedBy?: string | null; - /** Format: date-time */ - createdAt: string | null; - }; - TeamMembersResponse: { - members: components["schemas"]["TeamMember"][]; - }; - UpdateTeamRequest: { - name?: string; - profilePictureUrl?: string | null; - }; - UpdateTeamResponse: { - /** Format: uuid */ - id: string; - name: string; - profilePictureUrl?: string | null; - }; - AddTeamMemberRequest: { - /** Format: email */ - email: string; - }; - DefaultTemplateAlias: { - alias: string; - namespace?: string | null; - }; - DefaultTemplate: { - id: string; - aliases: components["schemas"]["DefaultTemplateAlias"][]; - /** Format: uuid */ - buildId: string; - /** Format: int64 */ - ramMb: number; - /** Format: int64 */ - vcpu: number; - /** Format: int64 */ - totalDiskSizeMb: number | null; - envdVersion?: string | null; - /** Format: date-time */ - createdAt: string; - public: boolean; - /** Format: int32 */ - buildCount: number; - /** Format: int64 */ - spawnCount: number; - }; - DefaultTemplatesResponse: { - templates: components["schemas"]["DefaultTemplate"][]; - }; - TeamResolveResponse: { - /** Format: uuid */ - id: string; - slug: string; - }; - }; - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - }; - parameters: { - /** @description Identifier of the build. */ - build_id: string; - /** @description Identifier of the sandbox. */ - sandboxID: string; - /** @description Maximum number of items to return per page. */ - builds_limit: number; - /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ - builds_cursor: string; - /** @description Optional filter by build identifier, template identifier, or template alias. */ - build_id_or_template: string; - /** @description Comma-separated list of build statuses to include. */ - build_statuses: components["schemas"]["BuildStatus"][]; - /** @description Comma-separated list of build IDs to get statuses for. */ - build_ids: string[]; - /** @description Identifier of the team. */ - teamId: string; - /** @description Identifier of the user. */ - userId: string; - /** @description Team slug to resolve. */ - teamSlug: string; - }; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + Error: { + /** + * Format: int32 + * @description Error code. + */ + code: number + /** @description Error message. */ + message: string + } + /** + * @description Build status mapped for dashboard clients. + * @enum {string} + */ + BuildStatus: 'building' | 'failed' | 'success' + ListedBuild: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string + /** @description Template alias when present, otherwise template ID. */ + template: string + /** @description Identifier of the template. */ + templateId: string + status: components['schemas']['BuildStatus'] + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + } + BuildsListResponse: { + data: components['schemas']['ListedBuild'][] + /** @description Cursor to pass to the next list request, or `null` if there is no next page. */ + nextCursor: string | null + } + BuildStatusItem: { + /** + * Format: uuid + * @description Identifier of the build. + */ + id: string + status: components['schemas']['BuildStatus'] + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + } + BuildsStatusesResponse: { + /** @description List of build statuses */ + buildStatuses: components['schemas']['BuildStatusItem'][] + } + BuildInfo: { + /** @description Template names related to this build, if available. */ + names?: string[] | null + /** + * Format: date-time + * @description Build creation timestamp in RFC3339 format. + */ + createdAt: string + /** + * Format: date-time + * @description Build completion timestamp in RFC3339 format, if finished. + */ + finishedAt: string | null + status: components['schemas']['BuildStatus'] + /** @description Failure message when status is `failed`, otherwise `null`. */ + statusMessage: string | null + } + /** + * Format: int64 + * @description CPU cores for the sandbox + */ + CPUCount: number + /** + * Format: int64 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number + /** + * Format: int64 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number + SandboxRecord: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string + /** @description Alias of the template */ + alias?: string + /** @description Identifier of the sandbox */ + sandboxID: string + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string + /** + * Format: date-time + * @description Time when the sandbox was stopped + */ + stoppedAt?: string | null + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + } + HealthResponse: { + /** @description Human-readable health check result. */ + message: string + } + UserTeamLimits: { + /** Format: int64 */ + maxLengthHours: number + /** Format: int32 */ + concurrentSandboxes: number + /** Format: int32 */ + concurrentTemplateBuilds: number + /** Format: int32 */ + maxVcpu: number + /** Format: int32 */ + maxRamMb: number + /** Format: int32 */ + diskMb: number + } + UserTeam: { + /** Format: uuid */ + id: string + name: string + slug: string + tier: string + email: string + profilePictureUrl: string | null + isDefault: boolean + limits: components['schemas']['UserTeamLimits'] + } + UserTeamsResponse: { + teams: components['schemas']['UserTeam'][] + } + TeamMember: { + /** Format: uuid */ + id: string + email: string + isDefault: boolean + /** Format: uuid */ + addedBy?: string | null + /** Format: date-time */ + createdAt: string | null + } + TeamMembersResponse: { + members: components['schemas']['TeamMember'][] + } + UpdateTeamRequest: { + name?: string + profilePictureUrl?: string | null + } + UpdateTeamResponse: { + /** Format: uuid */ + id: string + name: string + profilePictureUrl?: string | null + } + AddTeamMemberRequest: { + /** Format: email */ + email: string + } + DefaultTemplateAlias: { + alias: string + namespace?: string | null + } + DefaultTemplate: { + id: string + aliases: components['schemas']['DefaultTemplateAlias'][] + /** Format: uuid */ + buildId: string + /** Format: int64 */ + ramMb: number + /** Format: int64 */ + vcpu: number + /** Format: int64 */ + totalDiskSizeMb: number | null + envdVersion?: string | null + /** Format: date-time */ + createdAt: string + public: boolean + /** Format: int32 */ + buildCount: number + /** Format: int64 */ + spawnCount: number + } + DefaultTemplatesResponse: { + templates: components['schemas']['DefaultTemplate'][] + } + TeamResolveResponse: { + /** Format: uuid */ + id: string + slug: string + } + } + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + } + parameters: { + /** @description Identifier of the build. */ + build_id: string + /** @description Identifier of the sandbox. */ + sandboxID: string + /** @description Maximum number of items to return per page. */ + builds_limit: number + /** @description Cursor returned by the previous list response in `created_at|build_id` format. */ + builds_cursor: string + /** @description Optional filter by build identifier, template identifier, or template alias. */ + build_id_or_template: string + /** @description Comma-separated list of build statuses to include. */ + build_statuses: components['schemas']['BuildStatus'][] + /** @description Comma-separated list of build IDs to get statuses for. */ + build_ids: string[] + /** @description Identifier of the team. */ + teamId: string + /** @description Identifier of the user. */ + userId: string + /** @description Team slug to resolve. */ + teamSlug: string + } + requestBodies: never + headers: never + pathItems: never } -export type $defs = Record; -export type operations = Record; +export type $defs = Record +export type operations = Record diff --git a/src/core/shared/contracts/infra-api.types.ts b/src/core/shared/contracts/infra-api.types.ts index 4e4e31757..fb56ffef2 100644 --- a/src/core/shared/contracts/infra-api.types.ts +++ b/src/core/shared/contracts/infra-api.types.ts @@ -4,3216 +4,3219 @@ */ export interface paths { - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Health check */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Request was successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all teams */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all teams */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Team"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamID}/metrics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get metrics for the team */ - get: { - parameters: { - query?: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number; - end?: number; - }; - header?: never; - path: { - teamID: components["parameters"]["teamID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the team metrics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamMetric"][]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamID}/metrics/max": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get the maximum metrics for the team in the given interval */ - get: { - parameters: { - query: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number; - end?: number; - /** @description Metric to retrieve the maximum value for */ - metric: "concurrent_sandboxes" | "sandbox_start_rate"; - }; - header?: never; - path: { - teamID: components["parameters"]["teamID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the team metrics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["MaxTeamMetric"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all running sandboxes */ - get: { - parameters: { - query?: { - /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ - metadata?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all running sandboxes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListedSandbox"][]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** @description Create a sandbox from the template */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["NewSandbox"]; - }; - }; - responses: { - /** @description The sandbox was created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Sandbox"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v2/sandboxes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all sandboxes */ - get: { - parameters: { - query?: { - /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ - metadata?: string; - /** @description Filter sandboxes by one or more states */ - state?: components["schemas"]["SandboxState"][]; - /** @description Cursor to start the list from */ - nextToken?: components["parameters"]["paginationNextToken"]; - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all running sandboxes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListedSandbox"][]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/metrics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List metrics for given sandboxes */ - get: { - parameters: { - query: { - /** @description Comma-separated list of sandbox IDs to get metrics for */ - sandbox_ids: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all running sandboxes with metrics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxesWithMetrics"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/logs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * @deprecated - * @description Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. - */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - start?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - }; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox logs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxLogs"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v2/sandboxes/{sandboxID}/logs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get sandbox logs */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - cursor?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - /** @description Direction of the logs that should be returned */ - direction?: components["schemas"]["LogsDirection"]; - /** @description Minimum log level to return. Logs below this level are excluded */ - level?: components["schemas"]["LogLevel"]; - /** @description Case-sensitive substring match on log message content */ - search?: string; - }; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox logs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxLogsV2Response"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get a sandbox by id */ - get: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxDetail"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - /** @description Kill a sandbox */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The sandbox was killed successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/metrics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get sandbox metrics */ - get: { - parameters: { - query?: { - /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ - start?: number; - end?: number; - }; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the sandbox metrics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SandboxMetric"][]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/pause": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Pause the sandbox */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The sandbox was paused successfully and can be resumed */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 409: components["responses"]["409"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * @deprecated - * @description Resume the sandbox - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ResumedSandbox"]; - }; - }; - responses: { - /** @description The sandbox was resumed successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Sandbox"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 409: components["responses"]["409"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/connect": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ConnectSandbox"]; - }; - }; - responses: { - /** @description The sandbox was already running */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Sandbox"]; - }; - }; - /** @description The sandbox was resumed successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Sandbox"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/timeout": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Set the timeout for the sandbox. The sandbox will expire x seconds from the time of the request. Calling this method multiple times overwrites the TTL, each time using the current timestamp as the starting point to measure the timeout duration. */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": { - /** - * Format: int32 - * @description Timeout in seconds from the current time after which the sandbox should expire - */ - timeout: number; - }; - }; - }; - responses: { - /** @description Successfully set the sandbox timeout */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/refreshes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Refresh the sandbox extending its time to live */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": { - /** @description Duration for which the sandbox should be kept alive in seconds */ - duration?: number; - }; - }; - }; - responses: { - /** @description Successfully refreshed the sandbox */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/sandboxes/{sandboxID}/snapshots": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. */ - post: { - parameters: { - query?: never; - header?: never; - path: { - sandboxID: components["parameters"]["sandboxID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - /** @description Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. */ - name?: string; - }; - }; - }; - responses: { - /** @description Snapshot created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SnapshotInfo"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/snapshots": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all snapshots for the team */ - get: { - parameters: { - query?: { - sandboxID?: string; - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; - /** @description Cursor to start the list from */ - nextToken?: components["parameters"]["paginationNextToken"]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned snapshots */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SnapshotInfo"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v3/templates": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Create a new template */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildRequestV3"]; - }; - }; - responses: { - /** @description The build was requested successfully */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateRequestResponseV3"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v2/templates": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * @deprecated - * @description Create a new template - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildRequestV2"]; - }; - }; - responses: { - /** @description The build was requested successfully */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateLegacy"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/{templateID}/files/{hash}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get an upload link for a tar file containing build layer files */ - get: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - hash: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The upload link where to upload the tar file */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateBuildFileUpload"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all templates */ - get: { - parameters: { - query?: { - teamID?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all templates */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Template"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** - * @deprecated - * @description Create a new template - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildRequest"]; - }; - }; - responses: { - /** @description The build was accepted */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateLegacy"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/{templateID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all builds for a template */ - get: { - parameters: { - query?: { - /** @description Cursor to start the list from */ - nextToken?: components["parameters"]["paginationNextToken"]; - /** @description Maximum number of items to return per page */ - limit?: components["parameters"]["paginationLimit"]; - }; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the template with its builds */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateWithBuilds"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** - * @deprecated - * @description Rebuild an template - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildRequest"]; - }; - }; - responses: { - /** @description The build was accepted */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateLegacy"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - /** @description Delete a template */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The template was deleted successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - /** - * @deprecated - * @description Update template - */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateUpdateRequest"]; - }; - }; - responses: { - /** @description The template was updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - trace?: never; - }; - "/templates/{templateID}/builds/{buildID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * @deprecated - * @description Start the build - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The build has started */ - 202: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v2/templates/{templateID}/builds/{buildID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Start the build */ - post: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateBuildStartV2"]; - }; - }; - responses: { - /** @description The build has started */ - 202: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v2/templates/{templateID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** @description Update template */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TemplateUpdateRequest"]; - }; - }; - responses: { - /** @description The template was updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateUpdateResponse"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - trace?: never; - }; - "/templates/{templateID}/builds/{buildID}/status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get template build info */ - get: { - parameters: { - query?: { - /** @description Index of the starting build log that should be returned with the template */ - logsOffset?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - level?: components["schemas"]["LogLevel"]; - }; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the template */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateBuildInfo"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/{templateID}/builds/{buildID}/logs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get template build logs */ - get: { - parameters: { - query?: { - /** @description Starting timestamp of the logs that should be returned in milliseconds */ - cursor?: number; - /** @description Maximum number of logs that should be returned */ - limit?: number; - direction?: components["schemas"]["LogsDirection"]; - level?: components["schemas"]["LogLevel"]; - /** @description Source of the logs that should be returned from */ - source?: components["schemas"]["LogsSource"]; - }; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - buildID: components["parameters"]["buildID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the template build logs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateBuildLogsResponse"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/tags": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Assign tag(s) to a template build */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignTemplateTagsRequest"]; - }; - }; - responses: { - /** @description Tag assigned successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AssignedTemplateTags"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - /** @description Delete multiple tags from templates */ - delete: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteTemplateTagsRequest"]; - }; - }; - responses: { - /** @description Tags deleted successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/{templateID}/tags": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all tags for a template */ - get: { - parameters: { - query?: never; - header?: never; - path: { - templateID: components["parameters"]["templateID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the template tags */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateTag"][]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/templates/aliases/{alias}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Check if template with given alias exists */ - get: { - parameters: { - query?: never; - header?: never; - path: { - alias: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully queried template by alias */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TemplateAliasResponse"]; - }; - }; - 400: components["responses"]["400"]; - 403: components["responses"]["403"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/nodes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all nodes */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all nodes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Node"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/nodes/{nodeID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get node info */ - get: { - parameters: { - query?: { - /** @description Identifier of the cluster */ - clusterID?: string; - }; - header?: never; - path: { - nodeID: components["parameters"]["nodeID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned the node */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["NodeDetail"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** @description Change status of a node */ - post: { - parameters: { - query?: never; - header?: never; - path: { - nodeID: components["parameters"]["nodeID"]; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": components["schemas"]["NodeStatusChange"]; - }; - }; - responses: { - /** @description The node status was changed successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/teams/{teamID}/sandboxes/kill": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Kill all sandboxes for a team - * @description Kills all sandboxes for the specified team - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Team ID */ - teamID: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully killed sandboxes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AdminSandboxKillResult"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/teams/{teamID}/builds/cancel": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Cancel all builds for a team - * @description Cancels all in-progress and pending builds for the specified team - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Team ID */ - teamID: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully cancelled builds */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AdminBuildCancelResult"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/access-tokens": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Create a new access token */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["NewAccessToken"]; - }; - }; - responses: { - /** @description Access token created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreatedAccessToken"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/access-tokens/{accessTokenID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** @description Delete an access token */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - accessTokenID: components["parameters"]["accessTokenID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Access token deleted successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api-keys": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all team API keys */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully returned all team API keys */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TeamAPIKey"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** @description Create a new team API key */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["NewTeamAPIKey"]; - }; - }; - responses: { - /** @description Team API key created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreatedTeamAPIKey"]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api-keys/{apiKeyID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** @description Delete a team API key */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - apiKeyID: components["parameters"]["apiKeyID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Team API key deleted successfully */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - /** @description Update a team API key */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - apiKeyID: components["parameters"]["apiKeyID"]; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTeamAPIKey"]; - }; - }; - responses: { - /** @description Team API key updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - trace?: never; - }; - "/volumes": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description List all team volumes */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully listed all team volumes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Volume"][]; - }; - }; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - /** @description Create a new team volume */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["NewVolume"]; - }; - }; - responses: { - /** @description Successfully created a new team volume */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["VolumeAndToken"]; - }; - }; - 400: components["responses"]["400"]; - 401: components["responses"]["401"]; - 500: components["responses"]["500"]; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/volumes/{volumeID}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Get team volume info */ - get: { - parameters: { - query?: never; - header?: never; - path: { - volumeID: components["parameters"]["volumeID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved a team volume */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["VolumeAndToken"]; - }; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - put?: never; - post?: never; - /** @description Delete a team volume */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - volumeID: components["parameters"]["volumeID"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully deleted a team volume */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["401"]; - 404: components["responses"]["404"]; - 500: components["responses"]["500"]; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - Team: { - /** @description Identifier of the team */ - teamID: string; - /** @description Name of the team */ - name: string; - /** @description API key for the team */ - apiKey: string; - /** @description Whether the team is the default team */ - isDefault: boolean; - }; - TeamUser: { - /** - * Format: uuid - * @description Identifier of the user - */ - id: string; - /** @description Email of the user */ - email: string; - }; - TemplateUpdateRequest: { - /** @description Whether the template is public or only accessible by the team */ - public?: boolean; - }; - TemplateUpdateResponse: { - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[]; - }; - /** - * Format: int32 - * @description CPU cores for the sandbox - */ - CPUCount: number; - /** - * Format: int32 - * @description Memory for the sandbox in MiB - */ - MemoryMB: number; - /** - * Format: int32 - * @description Disk size for the sandbox in MiB - */ - DiskSizeMB: number; - /** @description Version of the envd running in the sandbox */ - EnvdVersion: string; - SandboxMetadata: { - [key: string]: string; - }; - /** - * @description State of the sandbox - * @enum {string} - */ - SandboxState: "running" | "paused"; - SnapshotInfo: { - /** @description Identifier of the snapshot template including the tag. Uses namespace/alias when a name was provided (e.g. team-slug/my-snapshot:default), otherwise falls back to the raw template ID (e.g. abc123:default). */ - snapshotID: string; - /** @description Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) */ - names: string[]; - }; - EnvVars: { - [key: string]: string; - }; - /** @description MCP configuration for the sandbox */ - Mcp: { - [key: string]: unknown; - } | null; - SandboxNetworkConfig: { - /** - * @description Specify if the sandbox URLs should be accessible only with authentication. - * @default true - */ - allowPublicTraffic: boolean; - /** @description List of allowed CIDR blocks or IP addresses for egress traffic. Allowed addresses always take precedence over blocked addresses. */ - allowOut?: string[]; - /** @description List of denied CIDR blocks or IP addresses for egress traffic */ - denyOut?: string[]; - /** @description Specify host mask which will be used for all sandbox requests */ - maskRequestHost?: string; - }; - /** - * @description Auto-resume enabled flag for paused sandboxes. Default false. - * @default false - */ - SandboxAutoResumeEnabled: boolean; - /** @description Auto-resume configuration for paused sandboxes. */ - SandboxAutoResumeConfig: { - enabled: components["schemas"]["SandboxAutoResumeEnabled"]; - }; - /** @description Log entry with timestamp and line */ - SandboxLog: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string; - /** @description Log line content */ - line: string; - }; - SandboxLogEntry: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string; - /** @description Log message content */ - message: string; - level: components["schemas"]["LogLevel"]; - fields: { - [key: string]: string; - }; - }; - SandboxLogs: { - /** @description Logs of the sandbox */ - logs: components["schemas"]["SandboxLog"][]; - /** @description Structured logs of the sandbox */ - logEntries: components["schemas"]["SandboxLogEntry"][]; - }; - SandboxLogsV2Response: { - /** - * @description Sandbox logs structured - * @default [] - */ - logs: components["schemas"]["SandboxLogEntry"][]; - }; - /** @description Metric entry with timestamp and line */ - SandboxMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string; - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number; - /** - * Format: int32 - * @description Number of CPU cores - */ - cpuCount: number; - /** - * Format: float - * @description CPU usage percentage - */ - cpuUsedPct: number; - /** - * Format: int64 - * @description Memory used in bytes - */ - memUsed: number; - /** - * Format: int64 - * @description Total memory in bytes - */ - memTotal: number; - /** - * Format: int64 - * @description Disk used in bytes - */ - diskUsed: number; - /** - * Format: int64 - * @description Total disk space in bytes - */ - diskTotal: number; - }; - SandboxVolumeMount: { - /** @description Name of the volume */ - name: string; - /** @description Path of the volume */ - path: string; - }; - Sandbox: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string; - /** @description Identifier of the sandbox */ - sandboxID: string; - /** @description Alias of the template */ - alias?: string; - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string; - envdVersion: components["schemas"]["EnvdVersion"]; - /** @description Access token used for envd communication */ - envdAccessToken?: string; - /** @description Token required for accessing sandbox via proxy. */ - trafficAccessToken?: string | null; - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null; - }; - SandboxDetail: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string; - /** @description Alias of the template */ - alias?: string; - /** @description Identifier of the sandbox */ - sandboxID: string; - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string; - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string; - /** - * Format: date-time - * @description Time when the sandbox will expire - */ - endAt: string; - envdVersion: components["schemas"]["EnvdVersion"]; - /** @description Access token used for envd communication */ - envdAccessToken?: string; - /** @description Base domain where the sandbox traffic is accessible */ - domain?: string | null; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - metadata?: components["schemas"]["SandboxMetadata"]; - state: components["schemas"]["SandboxState"]; - volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; - }; - ListedSandbox: { - /** @description Identifier of the template from which is the sandbox created */ - templateID: string; - /** @description Alias of the template */ - alias?: string; - /** @description Identifier of the sandbox */ - sandboxID: string; - /** - * @deprecated - * @description Identifier of the client - */ - clientID: string; - /** - * Format: date-time - * @description Time when the sandbox was started - */ - startedAt: string; - /** - * Format: date-time - * @description Time when the sandbox will expire - */ - endAt: string; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - metadata?: components["schemas"]["SandboxMetadata"]; - state: components["schemas"]["SandboxState"]; - envdVersion: components["schemas"]["EnvdVersion"]; - volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; - }; - SandboxesWithMetrics: { - sandboxes: { - [key: string]: components["schemas"]["SandboxMetric"]; - }; - }; - NewSandbox: { - /** @description Identifier of the required template */ - templateID: string; - /** - * Format: int32 - * @description Time to live for the sandbox in seconds. - * @default 15 - */ - timeout: number; - /** - * @description Automatically pauses the sandbox after the timeout - * @default false - */ - autoPause: boolean; - autoResume?: components["schemas"]["SandboxAutoResumeConfig"]; - /** @description Secure all system communication with sandbox */ - secure?: boolean; - /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ - allow_internet_access?: boolean; - network?: components["schemas"]["SandboxNetworkConfig"]; - metadata?: components["schemas"]["SandboxMetadata"]; - envVars?: components["schemas"]["EnvVars"]; - mcp?: components["schemas"]["Mcp"]; - volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; - }; - ResumedSandbox: { - /** - * Format: int32 - * @description Time to live for the sandbox in seconds. - * @default 15 - */ - timeout: number; - /** - * @deprecated - * @description Automatically pauses the sandbox after the timeout - */ - autoPause?: boolean; - }; - ConnectSandbox: { + '/health': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Health check */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Request was successful */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all teams */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all teams */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Team'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamID}/metrics': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get metrics for the team */ + get: { + parameters: { + query?: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number + end?: number + } + header?: never + path: { + teamID: components['parameters']['teamID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the team metrics */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamMetric'][] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/teams/{teamID}/metrics/max': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the maximum metrics for the team in the given interval */ + get: { + parameters: { + query: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number + end?: number + /** @description Metric to retrieve the maximum value for */ + metric: 'concurrent_sandboxes' | 'sandbox_start_rate' + } + header?: never + path: { + teamID: components['parameters']['teamID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the team metrics */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MaxTeamMetric'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all running sandboxes */ + get: { + parameters: { + query?: { + /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ + metadata?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all running sandboxes */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListedSandbox'][] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + /** @description Create a sandbox from the template */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['NewSandbox'] + } + } + responses: { + /** @description The sandbox was created successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Sandbox'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/sandboxes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all sandboxes */ + get: { + parameters: { + query?: { + /** @description Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. */ + metadata?: string + /** @description Filter sandboxes by one or more states */ + state?: components['schemas']['SandboxState'][] + /** @description Cursor to start the list from */ + nextToken?: components['parameters']['paginationNextToken'] + /** @description Maximum number of items to return per page */ + limit?: components['parameters']['paginationLimit'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all running sandboxes */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListedSandbox'][] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/metrics': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List metrics for given sandboxes */ + get: { + parameters: { + query: { + /** @description Comma-separated list of sandbox IDs to get metrics for */ + sandbox_ids: string[] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all running sandboxes with metrics */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxesWithMetrics'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/logs': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @deprecated + * @description Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. + */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + start?: number + /** @description Maximum number of logs that should be returned */ + limit?: number + } + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox logs */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxLogs'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/sandboxes/{sandboxID}/logs': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get sandbox logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number + /** @description Maximum number of logs that should be returned */ + limit?: number + /** @description Direction of the logs that should be returned */ + direction?: components['schemas']['LogsDirection'] + /** @description Minimum log level to return. Logs below this level are excluded */ + level?: components['schemas']['LogLevel'] + /** @description Case-sensitive substring match on log message content */ + search?: string + } + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox logs */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxLogsV2Response'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get a sandbox by id */ + get: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxDetail'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + /** @description Kill a sandbox */ + delete: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description The sandbox was killed successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/metrics': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get sandbox metrics */ + get: { + parameters: { + query?: { + /** @description Unix timestamp for the start of the interval, in seconds, for which the metrics */ + start?: number + end?: number + } + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the sandbox metrics */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SandboxMetric'][] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/pause': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Pause the sandbox */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description The sandbox was paused successfully and can be resumed */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/resume': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Resume the sandbox + */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ResumedSandbox'] + } + } + responses: { + /** @description The sandbox was resumed successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Sandbox'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/connect': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ConnectSandbox'] + } + } + responses: { + /** @description The sandbox was already running */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Sandbox'] + } + } + /** @description The sandbox was resumed successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Sandbox'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/timeout': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Set the timeout for the sandbox. The sandbox will expire x seconds from the time of the request. Calling this method multiple times overwrites the TTL, each time using the current timestamp as the starting point to measure the timeout duration. */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: { + content: { + 'application/json': { /** * Format: int32 * @description Timeout in seconds from the current time after which the sandbox should expire */ - timeout: number; - }; - /** @description Team metric with timestamp */ - TeamMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string; - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number; - /** - * Format: int32 - * @description The number of concurrent sandboxes for the team - */ - concurrentSandboxes: number; - /** - * Format: float - * @description Number of sandboxes started per second - */ - sandboxStartRate: number; - }; - /** @description Team metric with timestamp */ - MaxTeamMetric: { - /** - * Format: date-time - * @deprecated - * @description Timestamp of the metric entry - */ - timestamp: string; - /** - * Format: int64 - * @description Timestamp of the metric entry in Unix time (seconds since epoch) - */ - timestampUnix: number; - /** @description The maximum value of the requested metric in the given interval */ - value: number; - }; - AdminSandboxKillResult: { - /** @description Number of sandboxes successfully killed */ - killedCount: number; - /** @description Number of sandboxes that failed to kill */ - failedCount: number; - }; - AdminBuildCancelResult: { - /** @description Number of builds successfully cancelled */ - cancelledCount: number; - /** @description Number of builds that failed to cancel */ - failedCount: number; - }; - VolumeToken: { - token: string; - }; - Template: { - /** @description Identifier of the template */ - templateID: string; - /** @description Identifier of the last successful build for given template */ - buildID: string; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - /** @description Whether the template is public or only accessible by the team */ - public: boolean; - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[]; - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[]; - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string; - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string; - createdBy: components["schemas"]["TeamUser"] | null; - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null; - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number; - /** - * Format: int32 - * @description Number of times the template was built - */ - buildCount: number; - envdVersion: components["schemas"]["EnvdVersion"]; - buildStatus: components["schemas"]["TemplateBuildStatus"]; - }; - TemplateRequestResponseV3: { - /** @description Identifier of the template */ - templateID: string; - /** @description Identifier of the last successful build for given template */ - buildID: string; - /** @description Whether the template is public or only accessible by the team */ - public: boolean; - /** @description Names of the template */ - names: string[]; - /** @description Tags assigned to the template build */ - tags: string[]; - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[]; - }; - TemplateLegacy: { - /** @description Identifier of the template */ - templateID: string; - /** @description Identifier of the last successful build for given template */ - buildID: string; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB: components["schemas"]["DiskSizeMB"]; - /** @description Whether the template is public or only accessible by the team */ - public: boolean; - /** @description Aliases of the template */ - aliases: string[]; - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string; - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string; - createdBy: components["schemas"]["TeamUser"] | null; - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null; - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number; - /** - * Format: int32 - * @description Number of times the template was built - */ - buildCount: number; - envdVersion: components["schemas"]["EnvdVersion"]; - }; - TemplateBuild: { - /** - * Format: uuid - * @description Identifier of the build - */ - buildID: string; - status: components["schemas"]["TemplateBuildStatus"]; - /** - * Format: date-time - * @description Time when the build was created - */ - createdAt: string; - /** - * Format: date-time - * @description Time when the build was last updated - */ - updatedAt: string; - /** - * Format: date-time - * @description Time when the build was finished - */ - finishedAt?: string; - cpuCount: components["schemas"]["CPUCount"]; - memoryMB: components["schemas"]["MemoryMB"]; - diskSizeMB?: components["schemas"]["DiskSizeMB"]; - envdVersion?: components["schemas"]["EnvdVersion"]; - }; - TemplateWithBuilds: { - /** @description Identifier of the template */ - templateID: string; - /** @description Whether the template is public or only accessible by the team */ - public: boolean; - /** - * @deprecated - * @description Aliases of the template - */ - aliases: string[]; - /** @description Names of the template (namespace/alias format when namespaced) */ - names: string[]; - /** - * Format: date-time - * @description Time when the template was created - */ - createdAt: string; - /** - * Format: date-time - * @description Time when the template was last updated - */ - updatedAt: string; - /** - * Format: date-time - * @description Time when the template was last used - */ - lastSpawnedAt: string | null; - /** - * Format: int64 - * @description Number of times the template was used - */ - spawnCount: number; - /** @description List of builds for the template */ - builds: components["schemas"]["TemplateBuild"][]; - }; - TemplateAliasResponse: { - /** @description Identifier of the template */ - templateID: string; - /** @description Whether the template is public or only accessible by the team */ - public: boolean; - }; - TemplateBuildRequest: { - /** @description Alias of the template */ - alias?: string; - /** @description Dockerfile for the template */ - dockerfile: string; - /** @description Identifier of the team */ - teamID?: string; - /** @description Start command to execute in the template after the build */ - startCmd?: string; - /** @description Ready check command to execute in the template after the build */ - readyCmd?: string; - cpuCount?: components["schemas"]["CPUCount"]; - memoryMB?: components["schemas"]["MemoryMB"]; - }; - /** @description Step in the template build process */ - TemplateStep: { - /** @description Type of the step */ - type: string; - /** - * @description Arguments for the step - * @default [] - */ - args: string[]; - /** @description Hash of the files used in the step */ - filesHash?: string; - /** - * @description Whether the step should be forced to run regardless of the cache - * @default false - */ - force: boolean; - }; - TemplateBuildRequestV3: { - /** @description Name of the template. Can include a tag with colon separator (e.g. "my-template" or "my-template:v1"). If tag is included, it will be treated as if the tag was provided in the tags array. */ - name?: string; - /** @description Tags to assign to the template build */ - tags?: string[]; - /** - * @deprecated - * @description Alias of the template. Deprecated, use name instead. - */ - alias?: string; - /** - * @deprecated - * @description Identifier of the team - */ - teamID?: string; - cpuCount?: components["schemas"]["CPUCount"]; - memoryMB?: components["schemas"]["MemoryMB"]; - }; - TemplateBuildRequestV2: { - /** @description Alias of the template */ - alias: string; - /** - * @deprecated - * @description Identifier of the team - */ - teamID?: string; - cpuCount?: components["schemas"]["CPUCount"]; - memoryMB?: components["schemas"]["MemoryMB"]; - }; - FromImageRegistry: components["schemas"]["AWSRegistry"] | components["schemas"]["GCPRegistry"] | components["schemas"]["GeneralRegistry"]; - AWSRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: "aws"; - /** @description AWS Access Key ID for ECR authentication */ - awsAccessKeyId: string; - /** @description AWS Secret Access Key for ECR authentication */ - awsSecretAccessKey: string; - /** @description AWS Region where the ECR registry is located */ - awsRegion: string; - }; - GCPRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: "gcp"; - /** @description Service Account JSON for GCP authentication */ - serviceAccountJson: string; - }; - GeneralRegistry: { - /** - * @description Type of registry authentication (enum property replaced by openapi-typescript) - * @enum {string} - */ - type: "registry"; - /** @description Username to use for the registry */ - username: string; - /** @description Password to use for the registry */ - password: string; - }; - TemplateBuildStartV2: { - /** @description Image to use as a base for the template build */ - fromImage?: string; - /** @description Template to use as a base for the template build */ - fromTemplate?: string; - fromImageRegistry?: components["schemas"]["FromImageRegistry"]; - /** - * @description Whether the whole build should be forced to run regardless of the cache - * @default false - */ - force: boolean; - /** - * @description List of steps to execute in the template build - * @default [] - */ - steps: components["schemas"]["TemplateStep"][]; - /** @description Start command to execute in the template after the build */ - startCmd?: string; - /** @description Ready check command to execute in the template after the build */ - readyCmd?: string; - }; - TemplateBuildFileUpload: { - /** @description Whether the file is already present in the cache */ - present: boolean; - /** @description Url where the file should be uploaded to */ - url?: string; - }; - /** - * @description State of the sandbox - * @enum {string} - */ - LogLevel: "debug" | "info" | "warn" | "error"; - BuildLogEntry: { - /** - * Format: date-time - * @description Timestamp of the log entry - */ - timestamp: string; - /** @description Log message content */ - message: string; - level: components["schemas"]["LogLevel"]; - /** @description Step in the build process related to the log entry */ - step?: string; - }; - BuildStatusReason: { - /** @description Message with the status reason, currently reporting only for error status */ - message: string; - /** @description Step that failed */ - step?: string; - /** - * @description Log entries related to the status reason - * @default [] - */ - logEntries: components["schemas"]["BuildLogEntry"][]; - }; - /** - * @description Status of the template build - * @enum {string} - */ - TemplateBuildStatus: "building" | "waiting" | "ready" | "error"; - TemplateBuildInfo: { - /** - * @description Build logs - * @default [] - */ - logs: string[]; - /** - * @description Build logs structured - * @default [] - */ - logEntries: components["schemas"]["BuildLogEntry"][]; - /** @description Identifier of the template */ - templateID: string; - /** @description Identifier of the build */ - buildID: string; - status: components["schemas"]["TemplateBuildStatus"]; - reason?: components["schemas"]["BuildStatusReason"]; - }; - TemplateBuildLogsResponse: { - /** - * @description Build logs structured - * @default [] - */ - logs: components["schemas"]["BuildLogEntry"][]; - }; - /** - * @description Direction of the logs that should be returned - * @enum {string} - */ - LogsDirection: "forward" | "backward"; - /** - * @description Source of the logs that should be returned - * @enum {string} - */ - LogsSource: "temporary" | "persistent"; - /** - * @description Status of the node - * @enum {string} - */ - NodeStatus: "ready" | "draining" | "connecting" | "unhealthy"; - NodeStatusChange: { - /** - * Format: uuid - * @description Identifier of the cluster - */ - clusterID?: string; - status: components["schemas"]["NodeStatus"]; - }; - DiskMetrics: { - /** @description Mount point of the disk */ - mountPoint: string; - /** @description Device name */ - device: string; - /** @description Filesystem type (e.g., ext4, xfs) */ - filesystemType: string; - /** - * Format: uint64 - * @description Used space in bytes - */ - usedBytes: number; - /** - * Format: uint64 - * @description Total space in bytes - */ - totalBytes: number; - }; - /** @description Node metrics */ - NodeMetrics: { - /** - * Format: uint32 - * @description Number of allocated CPU cores - */ - allocatedCPU: number; - /** - * Format: uint32 - * @description Node CPU usage percentage - */ - cpuPercent: number; - /** - * Format: uint32 - * @description Total number of CPU cores on the node - */ - cpuCount: number; - /** - * Format: uint64 - * @description Amount of allocated memory in bytes - */ - allocatedMemoryBytes: number; - /** - * Format: uint64 - * @description Node memory used in bytes - */ - memoryUsedBytes: number; - /** - * Format: uint64 - * @description Total node memory in bytes - */ - memoryTotalBytes: number; - /** @description Detailed metrics for each disk/mount point */ - disks: components["schemas"]["DiskMetrics"][]; - }; - MachineInfo: { - /** @description CPU family of the node */ - cpuFamily: string; - /** @description CPU model of the node */ - cpuModel: string; - /** @description CPU model name of the node */ - cpuModelName: string; - /** @description CPU architecture of the node */ - cpuArchitecture: string; - }; - Node: { - /** @description Version of the orchestrator */ - version: string; - /** @description Commit of the orchestrator */ - commit: string; - /** @description Identifier of the node */ - id: string; - /** @description Service instance identifier of the node */ - serviceInstanceID: string; - /** @description Identifier of the cluster */ - clusterID: string; - machineInfo: components["schemas"]["MachineInfo"]; - status: components["schemas"]["NodeStatus"]; - /** - * Format: uint32 - * @description Number of sandboxes running on the node - */ - sandboxCount: number; - metrics: components["schemas"]["NodeMetrics"]; - /** - * Format: uint64 - * @description Number of sandbox create successes - */ - createSuccesses: number; - /** - * Format: uint64 - * @description Number of sandbox create fails - */ - createFails: number; - /** - * Format: int - * @description Number of starting Sandboxes - */ - sandboxStartingCount: number; - }; - NodeDetail: { - /** @description Identifier of the cluster */ - clusterID: string; - /** @description Version of the orchestrator */ - version: string; - /** @description Commit of the orchestrator */ - commit: string; - /** @description Identifier of the node */ - id: string; - /** @description Service instance identifier of the node */ - serviceInstanceID: string; - machineInfo: components["schemas"]["MachineInfo"]; - status: components["schemas"]["NodeStatus"]; - /** - * Format: uint32 - * @description Number of sandboxes running on the node - */ - sandboxCount: number; - metrics: components["schemas"]["NodeMetrics"]; - /** @description List of cached builds id on the node */ - cachedBuilds: string[]; - /** - * Format: uint64 - * @description Number of sandbox create successes - */ - createSuccesses: number; - /** - * Format: uint64 - * @description Number of sandbox create fails - */ - createFails: number; - }; - CreatedAccessToken: { - /** - * Format: uuid - * @description Identifier of the access token - */ - id: string; - /** @description Name of the access token */ - name: string; - /** @description The fully created access token */ - token: string; - mask: components["schemas"]["IdentifierMaskingDetails"]; - /** - * Format: date-time - * @description Timestamp of access token creation - */ - createdAt: string; - }; - NewAccessToken: { - /** @description Name of the access token */ - name: string; - }; - TeamAPIKey: { - /** - * Format: uuid - * @description Identifier of the API key - */ - id: string; - /** @description Name of the API key */ - name: string; - mask: components["schemas"]["IdentifierMaskingDetails"]; - /** - * Format: date-time - * @description Timestamp of API key creation - */ - createdAt: string; - createdBy?: components["schemas"]["TeamUser"] | null; - /** - * Format: date-time - * @description Last time this API key was used - */ - lastUsed?: string | null; - }; - CreatedTeamAPIKey: { - /** - * Format: uuid - * @description Identifier of the API key - */ - id: string; - /** @description Raw value of the API key */ - key: string; - mask: components["schemas"]["IdentifierMaskingDetails"]; - /** @description Name of the API key */ - name: string; - /** - * Format: date-time - * @description Timestamp of API key creation - */ - createdAt: string; - createdBy?: components["schemas"]["TeamUser"] | null; - /** - * Format: date-time - * @description Last time this API key was used - */ - lastUsed?: string | null; - }; - NewTeamAPIKey: { - /** @description Name of the API key */ - name: string; - }; - UpdateTeamAPIKey: { - /** @description New name for the API key */ - name: string; - }; - AssignedTemplateTags: { - /** @description Assigned tags of the template */ - tags: string[]; - /** - * Format: uuid - * @description Identifier of the build associated with these tags - */ - buildID: string; - }; - TemplateTag: { - /** @description The tag name */ - tag: string; - /** - * Format: uuid - * @description Identifier of the build associated with this tag - */ - buildID: string; - /** - * Format: date-time - * @description Time when the tag was assigned - */ - createdAt: string; - }; - AssignTemplateTagsRequest: { - /** @description Target template in "name:tag" format */ - target: string; - /** @description Tags to assign to the template */ - tags: string[]; - }; - DeleteTemplateTagsRequest: { - /** @description Name of the template */ - name: string; - /** @description Tags to delete */ - tags: string[]; - }; - Error: { - /** - * Format: int32 - * @description Error code - */ - code: number; - /** @description Error */ - message: string; - }; - IdentifierMaskingDetails: { - /** @description Prefix that identifies the token or key type */ - prefix: string; - /** @description Length of the token or key */ - valueLength: number; - /** @description Prefix used in masked version of the token or key */ - maskedValuePrefix: string; - /** @description Suffix used in masked version of the token or key */ - maskedValueSuffix: string; - }; - Volume: { - /** @description ID of the volume */ - volumeID: string; - /** @description Name of the volume */ - name: string; - }; - VolumeAndToken: { - /** @description ID of the volume */ - volumeID: string; - /** @description Name of the volume */ - name: string; - /** @description Auth token to use for interacting with volume content */ - token: string; - }; - NewVolume: { - /** @description Name of the volume */ - name: string; - }; - }; - responses: { - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Authentication error */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Error"]; - }; - }; - }; + timeout: number + } + } + } + responses: { + /** @description Successfully set the sandbox timeout */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/refreshes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Refresh the sandbox extending its time to live */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody?: { + content: { + 'application/json': { + /** @description Duration for which the sandbox should be kept alive in seconds */ + duration?: number + } + } + } + responses: { + /** @description Successfully refreshed the sandbox */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/sandboxes/{sandboxID}/snapshots': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Create a persistent snapshot from the sandbox's current state. Snapshots can be used to create new sandboxes and persist beyond the original sandbox's lifetime. */ + post: { + parameters: { + query?: never + header?: never + path: { + sandboxID: components['parameters']['sandboxID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': { + /** @description Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. */ + name?: string + } + } + } + responses: { + /** @description Snapshot created successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SnapshotInfo'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/snapshots': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all snapshots for the team */ + get: { + parameters: { + query?: { + sandboxID?: string + /** @description Maximum number of items to return per page */ + limit?: components['parameters']['paginationLimit'] + /** @description Cursor to start the list from */ + nextToken?: components['parameters']['paginationNextToken'] + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned snapshots */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SnapshotInfo'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v3/templates': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Create a new template */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateBuildRequestV3'] + } + } + responses: { + /** @description The build was requested successfully */ + 202: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateRequestResponseV3'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/templates': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Create a new template + */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateBuildRequestV2'] + } + } + responses: { + /** @description The build was requested successfully */ + 202: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateLegacy'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/{templateID}/files/{hash}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get an upload link for a tar file containing build layer files */ + get: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + hash: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description The upload link where to upload the tar file */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateBuildFileUpload'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all templates */ + get: { + parameters: { + query?: { + teamID?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all templates */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Template'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + /** + * @deprecated + * @description Create a new template + */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateBuildRequest'] + } + } + responses: { + /** @description The build was accepted */ + 202: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateLegacy'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/{templateID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all builds for a template */ + get: { + parameters: { + query?: { + /** @description Cursor to start the list from */ + nextToken?: components['parameters']['paginationNextToken'] + /** @description Maximum number of items to return per page */ + limit?: components['parameters']['paginationLimit'] + } + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the template with its builds */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateWithBuilds'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + /** + * @deprecated + * @description Rebuild an template + */ + post: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateBuildRequest'] + } + } + responses: { + /** @description The build was accepted */ + 202: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateLegacy'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + /** @description Delete a template */ + delete: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description The template was deleted successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + /** + * @deprecated + * @description Update template + */ + patch: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateUpdateRequest'] + } + } + responses: { + /** @description The template was updated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + trace?: never + } + '/templates/{templateID}/builds/{buildID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Start the build + */ + post: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + buildID: components['parameters']['buildID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description The build has started */ + 202: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/templates/{templateID}/builds/{buildID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Start the build */ + post: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + buildID: components['parameters']['buildID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateBuildStartV2'] + } + } + responses: { + /** @description The build has started */ + 202: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/templates/{templateID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** @description Update template */ + patch: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['TemplateUpdateRequest'] + } + } + responses: { + /** @description The template was updated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateUpdateResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + trace?: never + } + '/templates/{templateID}/builds/{buildID}/status': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get template build info */ + get: { + parameters: { + query?: { + /** @description Index of the starting build log that should be returned with the template */ + logsOffset?: number + /** @description Maximum number of logs that should be returned */ + limit?: number + level?: components['schemas']['LogLevel'] + } + header?: never + path: { + templateID: components['parameters']['templateID'] + buildID: components['parameters']['buildID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the template */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateBuildInfo'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/{templateID}/builds/{buildID}/logs': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get template build logs */ + get: { + parameters: { + query?: { + /** @description Starting timestamp of the logs that should be returned in milliseconds */ + cursor?: number + /** @description Maximum number of logs that should be returned */ + limit?: number + direction?: components['schemas']['LogsDirection'] + level?: components['schemas']['LogLevel'] + /** @description Source of the logs that should be returned from */ + source?: components['schemas']['LogsSource'] + } + header?: never + path: { + templateID: components['parameters']['templateID'] + buildID: components['parameters']['buildID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the template build logs */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateBuildLogsResponse'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/tags': { parameters: { - templateID: string; - buildID: string; - sandboxID: string; - teamID: string; - nodeID: string; - apiKeyID: string; - accessTokenID: string; - snapshotID: string; - tag: string; - /** @description Maximum number of items to return per page */ - paginationLimit: number; - /** @description Cursor to start the list from */ - paginationNextToken: string; - volumeID: string; - }; - requestBodies: never; - headers: never; - pathItems: never; + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Assign tag(s) to a template build */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AssignTemplateTagsRequest'] + } + } + responses: { + /** @description Tag assigned successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AssignedTemplateTags'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + /** @description Delete multiple tags from templates */ + delete: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['DeleteTemplateTagsRequest'] + } + } + responses: { + /** @description Tags deleted successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/{templateID}/tags': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all tags for a template */ + get: { + parameters: { + query?: never + header?: never + path: { + templateID: components['parameters']['templateID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the template tags */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateTag'][] + } + } + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/templates/aliases/{alias}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Check if template with given alias exists */ + get: { + parameters: { + query?: never + header?: never + path: { + alias: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully queried template by alias */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TemplateAliasResponse'] + } + } + 400: components['responses']['400'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/nodes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all nodes */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all nodes */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Node'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/nodes/{nodeID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get node info */ + get: { + parameters: { + query?: { + /** @description Identifier of the cluster */ + clusterID?: string + } + header?: never + path: { + nodeID: components['parameters']['nodeID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned the node */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['NodeDetail'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + /** @description Change status of a node */ + post: { + parameters: { + query?: never + header?: never + path: { + nodeID: components['parameters']['nodeID'] + } + cookie?: never + } + requestBody?: { + content: { + 'application/json': components['schemas']['NodeStatusChange'] + } + } + responses: { + /** @description The node status was changed successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/admin/teams/{teamID}/sandboxes/kill': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Kill all sandboxes for a team + * @description Kills all sandboxes for the specified team + */ + post: { + parameters: { + query?: never + header?: never + path: { + /** @description Team ID */ + teamID: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully killed sandboxes */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AdminSandboxKillResult'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/admin/teams/{teamID}/builds/cancel': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Cancel all builds for a team + * @description Cancels all in-progress and pending builds for the specified team + */ + post: { + parameters: { + query?: never + header?: never + path: { + /** @description Team ID */ + teamID: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully cancelled builds */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AdminBuildCancelResult'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/access-tokens': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Create a new access token */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['NewAccessToken'] + } + } + responses: { + /** @description Access token created successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CreatedAccessToken'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/access-tokens/{accessTokenID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** @description Delete an access token */ + delete: { + parameters: { + query?: never + header?: never + path: { + accessTokenID: components['parameters']['accessTokenID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Access token deleted successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/api-keys': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all team API keys */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all team API keys */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamAPIKey'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + /** @description Create a new team API key */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['NewTeamAPIKey'] + } + } + responses: { + /** @description Team API key created successfully */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CreatedTeamAPIKey'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/api-keys/{apiKeyID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** @description Delete a team API key */ + delete: { + parameters: { + query?: never + header?: never + path: { + apiKeyID: components['parameters']['apiKeyID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Team API key deleted successfully */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + /** @description Update a team API key */ + patch: { + parameters: { + query?: never + header?: never + path: { + apiKeyID: components['parameters']['apiKeyID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateTeamAPIKey'] + } + } + responses: { + /** @description Team API key updated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + trace?: never + } + '/volumes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all team volumes */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully listed all team volumes */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Volume'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + /** @description Create a new team volume */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['NewVolume'] + } + } + responses: { + /** @description Successfully created a new team volume */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['VolumeAndToken'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/volumes/{volumeID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get team volume info */ + get: { + parameters: { + query?: never + header?: never + path: { + volumeID: components['parameters']['volumeID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully retrieved a team volume */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['VolumeAndToken'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + /** @description Delete a team volume */ + delete: { + parameters: { + query?: never + header?: never + path: { + volumeID: components['parameters']['volumeID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully deleted a team volume */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } +} +export type webhooks = Record +export interface components { + schemas: { + Team: { + /** @description Identifier of the team */ + teamID: string + /** @description Name of the team */ + name: string + /** @description API key for the team */ + apiKey: string + /** @description Whether the team is the default team */ + isDefault: boolean + } + TeamUser: { + /** + * Format: uuid + * @description Identifier of the user + */ + id: string + /** @description Email of the user */ + email: string + } + TemplateUpdateRequest: { + /** @description Whether the template is public or only accessible by the team */ + public?: boolean + } + TemplateUpdateResponse: { + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[] + } + /** + * Format: int32 + * @description CPU cores for the sandbox + */ + CPUCount: number + /** + * Format: int32 + * @description Memory for the sandbox in MiB + */ + MemoryMB: number + /** + * Format: int32 + * @description Disk size for the sandbox in MiB + */ + DiskSizeMB: number + /** @description Version of the envd running in the sandbox */ + EnvdVersion: string + SandboxMetadata: { + [key: string]: string + } + /** + * @description State of the sandbox + * @enum {string} + */ + SandboxState: 'running' | 'paused' + SnapshotInfo: { + /** @description Identifier of the snapshot template including the tag. Uses namespace/alias when a name was provided (e.g. team-slug/my-snapshot:default), otherwise falls back to the raw template ID (e.g. abc123:default). */ + snapshotID: string + /** @description Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2) */ + names: string[] + } + EnvVars: { + [key: string]: string + } + /** @description MCP configuration for the sandbox */ + Mcp: { + [key: string]: unknown + } | null + SandboxNetworkConfig: { + /** + * @description Specify if the sandbox URLs should be accessible only with authentication. + * @default true + */ + allowPublicTraffic: boolean + /** @description List of allowed CIDR blocks or IP addresses for egress traffic. Allowed addresses always take precedence over blocked addresses. */ + allowOut?: string[] + /** @description List of denied CIDR blocks or IP addresses for egress traffic */ + denyOut?: string[] + /** @description Specify host mask which will be used for all sandbox requests */ + maskRequestHost?: string + } + /** + * @description Auto-resume enabled flag for paused sandboxes. Default false. + * @default false + */ + SandboxAutoResumeEnabled: boolean + /** @description Auto-resume configuration for paused sandboxes. */ + SandboxAutoResumeConfig: { + enabled: components['schemas']['SandboxAutoResumeEnabled'] + } + /** @description Log entry with timestamp and line */ + SandboxLog: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string + /** @description Log line content */ + line: string + } + SandboxLogEntry: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string + /** @description Log message content */ + message: string + level: components['schemas']['LogLevel'] + fields: { + [key: string]: string + } + } + SandboxLogs: { + /** @description Logs of the sandbox */ + logs: components['schemas']['SandboxLog'][] + /** @description Structured logs of the sandbox */ + logEntries: components['schemas']['SandboxLogEntry'][] + } + SandboxLogsV2Response: { + /** + * @description Sandbox logs structured + * @default [] + */ + logs: components['schemas']['SandboxLogEntry'][] + } + /** @description Metric entry with timestamp and line */ + SandboxMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number + /** + * Format: int32 + * @description Number of CPU cores + */ + cpuCount: number + /** + * Format: float + * @description CPU usage percentage + */ + cpuUsedPct: number + /** + * Format: int64 + * @description Memory used in bytes + */ + memUsed: number + /** + * Format: int64 + * @description Total memory in bytes + */ + memTotal: number + /** + * Format: int64 + * @description Disk used in bytes + */ + diskUsed: number + /** + * Format: int64 + * @description Total disk space in bytes + */ + diskTotal: number + } + SandboxVolumeMount: { + /** @description Name of the volume */ + name: string + /** @description Path of the volume */ + path: string + } + Sandbox: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string + /** @description Identifier of the sandbox */ + sandboxID: string + /** @description Alias of the template */ + alias?: string + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string + envdVersion: components['schemas']['EnvdVersion'] + /** @description Access token used for envd communication */ + envdAccessToken?: string + /** @description Token required for accessing sandbox via proxy. */ + trafficAccessToken?: string | null + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null + } + SandboxDetail: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string + /** @description Alias of the template */ + alias?: string + /** @description Identifier of the sandbox */ + sandboxID: string + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string + /** + * Format: date-time + * @description Time when the sandbox will expire + */ + endAt: string + envdVersion: components['schemas']['EnvdVersion'] + /** @description Access token used for envd communication */ + envdAccessToken?: string + /** @description Base domain where the sandbox traffic is accessible */ + domain?: string | null + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + metadata?: components['schemas']['SandboxMetadata'] + state: components['schemas']['SandboxState'] + volumeMounts?: components['schemas']['SandboxVolumeMount'][] + } + ListedSandbox: { + /** @description Identifier of the template from which is the sandbox created */ + templateID: string + /** @description Alias of the template */ + alias?: string + /** @description Identifier of the sandbox */ + sandboxID: string + /** + * @deprecated + * @description Identifier of the client + */ + clientID: string + /** + * Format: date-time + * @description Time when the sandbox was started + */ + startedAt: string + /** + * Format: date-time + * @description Time when the sandbox will expire + */ + endAt: string + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + metadata?: components['schemas']['SandboxMetadata'] + state: components['schemas']['SandboxState'] + envdVersion: components['schemas']['EnvdVersion'] + volumeMounts?: components['schemas']['SandboxVolumeMount'][] + } + SandboxesWithMetrics: { + sandboxes: { + [key: string]: components['schemas']['SandboxMetric'] + } + } + NewSandbox: { + /** @description Identifier of the required template */ + templateID: string + /** + * Format: int32 + * @description Time to live for the sandbox in seconds. + * @default 15 + */ + timeout: number + /** + * @description Automatically pauses the sandbox after the timeout + * @default false + */ + autoPause: boolean + autoResume?: components['schemas']['SandboxAutoResumeConfig'] + /** @description Secure all system communication with sandbox */ + secure?: boolean + /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ + allow_internet_access?: boolean + network?: components['schemas']['SandboxNetworkConfig'] + metadata?: components['schemas']['SandboxMetadata'] + envVars?: components['schemas']['EnvVars'] + mcp?: components['schemas']['Mcp'] + volumeMounts?: components['schemas']['SandboxVolumeMount'][] + } + ResumedSandbox: { + /** + * Format: int32 + * @description Time to live for the sandbox in seconds. + * @default 15 + */ + timeout: number + /** + * @deprecated + * @description Automatically pauses the sandbox after the timeout + */ + autoPause?: boolean + } + ConnectSandbox: { + /** + * Format: int32 + * @description Timeout in seconds from the current time after which the sandbox should expire + */ + timeout: number + } + /** @description Team metric with timestamp */ + TeamMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number + /** + * Format: int32 + * @description The number of concurrent sandboxes for the team + */ + concurrentSandboxes: number + /** + * Format: float + * @description Number of sandboxes started per second + */ + sandboxStartRate: number + } + /** @description Team metric with timestamp */ + MaxTeamMetric: { + /** + * Format: date-time + * @deprecated + * @description Timestamp of the metric entry + */ + timestamp: string + /** + * Format: int64 + * @description Timestamp of the metric entry in Unix time (seconds since epoch) + */ + timestampUnix: number + /** @description The maximum value of the requested metric in the given interval */ + value: number + } + AdminSandboxKillResult: { + /** @description Number of sandboxes successfully killed */ + killedCount: number + /** @description Number of sandboxes that failed to kill */ + failedCount: number + } + AdminBuildCancelResult: { + /** @description Number of builds successfully cancelled */ + cancelledCount: number + /** @description Number of builds that failed to cancel */ + failedCount: number + } + VolumeToken: { + token: string + } + Template: { + /** @description Identifier of the template */ + templateID: string + /** @description Identifier of the last successful build for given template */ + buildID: string + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + /** @description Whether the template is public or only accessible by the team */ + public: boolean + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[] + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[] + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string + createdBy: components['schemas']['TeamUser'] | null + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number + /** + * Format: int32 + * @description Number of times the template was built + */ + buildCount: number + envdVersion: components['schemas']['EnvdVersion'] + buildStatus: components['schemas']['TemplateBuildStatus'] + } + TemplateRequestResponseV3: { + /** @description Identifier of the template */ + templateID: string + /** @description Identifier of the last successful build for given template */ + buildID: string + /** @description Whether the template is public or only accessible by the team */ + public: boolean + /** @description Names of the template */ + names: string[] + /** @description Tags assigned to the template build */ + tags: string[] + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[] + } + TemplateLegacy: { + /** @description Identifier of the template */ + templateID: string + /** @description Identifier of the last successful build for given template */ + buildID: string + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB: components['schemas']['DiskSizeMB'] + /** @description Whether the template is public or only accessible by the team */ + public: boolean + /** @description Aliases of the template */ + aliases: string[] + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string + createdBy: components['schemas']['TeamUser'] | null + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number + /** + * Format: int32 + * @description Number of times the template was built + */ + buildCount: number + envdVersion: components['schemas']['EnvdVersion'] + } + TemplateBuild: { + /** + * Format: uuid + * @description Identifier of the build + */ + buildID: string + status: components['schemas']['TemplateBuildStatus'] + /** + * Format: date-time + * @description Time when the build was created + */ + createdAt: string + /** + * Format: date-time + * @description Time when the build was last updated + */ + updatedAt: string + /** + * Format: date-time + * @description Time when the build was finished + */ + finishedAt?: string + cpuCount: components['schemas']['CPUCount'] + memoryMB: components['schemas']['MemoryMB'] + diskSizeMB?: components['schemas']['DiskSizeMB'] + envdVersion?: components['schemas']['EnvdVersion'] + } + TemplateWithBuilds: { + /** @description Identifier of the template */ + templateID: string + /** @description Whether the template is public or only accessible by the team */ + public: boolean + /** + * @deprecated + * @description Aliases of the template + */ + aliases: string[] + /** @description Names of the template (namespace/alias format when namespaced) */ + names: string[] + /** + * Format: date-time + * @description Time when the template was created + */ + createdAt: string + /** + * Format: date-time + * @description Time when the template was last updated + */ + updatedAt: string + /** + * Format: date-time + * @description Time when the template was last used + */ + lastSpawnedAt: string | null + /** + * Format: int64 + * @description Number of times the template was used + */ + spawnCount: number + /** @description List of builds for the template */ + builds: components['schemas']['TemplateBuild'][] + } + TemplateAliasResponse: { + /** @description Identifier of the template */ + templateID: string + /** @description Whether the template is public or only accessible by the team */ + public: boolean + } + TemplateBuildRequest: { + /** @description Alias of the template */ + alias?: string + /** @description Dockerfile for the template */ + dockerfile: string + /** @description Identifier of the team */ + teamID?: string + /** @description Start command to execute in the template after the build */ + startCmd?: string + /** @description Ready check command to execute in the template after the build */ + readyCmd?: string + cpuCount?: components['schemas']['CPUCount'] + memoryMB?: components['schemas']['MemoryMB'] + } + /** @description Step in the template build process */ + TemplateStep: { + /** @description Type of the step */ + type: string + /** + * @description Arguments for the step + * @default [] + */ + args: string[] + /** @description Hash of the files used in the step */ + filesHash?: string + /** + * @description Whether the step should be forced to run regardless of the cache + * @default false + */ + force: boolean + } + TemplateBuildRequestV3: { + /** @description Name of the template. Can include a tag with colon separator (e.g. "my-template" or "my-template:v1"). If tag is included, it will be treated as if the tag was provided in the tags array. */ + name?: string + /** @description Tags to assign to the template build */ + tags?: string[] + /** + * @deprecated + * @description Alias of the template. Deprecated, use name instead. + */ + alias?: string + /** + * @deprecated + * @description Identifier of the team + */ + teamID?: string + cpuCount?: components['schemas']['CPUCount'] + memoryMB?: components['schemas']['MemoryMB'] + } + TemplateBuildRequestV2: { + /** @description Alias of the template */ + alias: string + /** + * @deprecated + * @description Identifier of the team + */ + teamID?: string + cpuCount?: components['schemas']['CPUCount'] + memoryMB?: components['schemas']['MemoryMB'] + } + FromImageRegistry: + | components['schemas']['AWSRegistry'] + | components['schemas']['GCPRegistry'] + | components['schemas']['GeneralRegistry'] + AWSRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: 'aws' + /** @description AWS Access Key ID for ECR authentication */ + awsAccessKeyId: string + /** @description AWS Secret Access Key for ECR authentication */ + awsSecretAccessKey: string + /** @description AWS Region where the ECR registry is located */ + awsRegion: string + } + GCPRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: 'gcp' + /** @description Service Account JSON for GCP authentication */ + serviceAccountJson: string + } + GeneralRegistry: { + /** + * @description Type of registry authentication (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: 'registry' + /** @description Username to use for the registry */ + username: string + /** @description Password to use for the registry */ + password: string + } + TemplateBuildStartV2: { + /** @description Image to use as a base for the template build */ + fromImage?: string + /** @description Template to use as a base for the template build */ + fromTemplate?: string + fromImageRegistry?: components['schemas']['FromImageRegistry'] + /** + * @description Whether the whole build should be forced to run regardless of the cache + * @default false + */ + force: boolean + /** + * @description List of steps to execute in the template build + * @default [] + */ + steps: components['schemas']['TemplateStep'][] + /** @description Start command to execute in the template after the build */ + startCmd?: string + /** @description Ready check command to execute in the template after the build */ + readyCmd?: string + } + TemplateBuildFileUpload: { + /** @description Whether the file is already present in the cache */ + present: boolean + /** @description Url where the file should be uploaded to */ + url?: string + } + /** + * @description State of the sandbox + * @enum {string} + */ + LogLevel: 'debug' | 'info' | 'warn' | 'error' + BuildLogEntry: { + /** + * Format: date-time + * @description Timestamp of the log entry + */ + timestamp: string + /** @description Log message content */ + message: string + level: components['schemas']['LogLevel'] + /** @description Step in the build process related to the log entry */ + step?: string + } + BuildStatusReason: { + /** @description Message with the status reason, currently reporting only for error status */ + message: string + /** @description Step that failed */ + step?: string + /** + * @description Log entries related to the status reason + * @default [] + */ + logEntries: components['schemas']['BuildLogEntry'][] + } + /** + * @description Status of the template build + * @enum {string} + */ + TemplateBuildStatus: 'building' | 'waiting' | 'ready' | 'error' + TemplateBuildInfo: { + /** + * @description Build logs + * @default [] + */ + logs: string[] + /** + * @description Build logs structured + * @default [] + */ + logEntries: components['schemas']['BuildLogEntry'][] + /** @description Identifier of the template */ + templateID: string + /** @description Identifier of the build */ + buildID: string + status: components['schemas']['TemplateBuildStatus'] + reason?: components['schemas']['BuildStatusReason'] + } + TemplateBuildLogsResponse: { + /** + * @description Build logs structured + * @default [] + */ + logs: components['schemas']['BuildLogEntry'][] + } + /** + * @description Direction of the logs that should be returned + * @enum {string} + */ + LogsDirection: 'forward' | 'backward' + /** + * @description Source of the logs that should be returned + * @enum {string} + */ + LogsSource: 'temporary' | 'persistent' + /** + * @description Status of the node + * @enum {string} + */ + NodeStatus: 'ready' | 'draining' | 'connecting' | 'unhealthy' + NodeStatusChange: { + /** + * Format: uuid + * @description Identifier of the cluster + */ + clusterID?: string + status: components['schemas']['NodeStatus'] + } + DiskMetrics: { + /** @description Mount point of the disk */ + mountPoint: string + /** @description Device name */ + device: string + /** @description Filesystem type (e.g., ext4, xfs) */ + filesystemType: string + /** + * Format: uint64 + * @description Used space in bytes + */ + usedBytes: number + /** + * Format: uint64 + * @description Total space in bytes + */ + totalBytes: number + } + /** @description Node metrics */ + NodeMetrics: { + /** + * Format: uint32 + * @description Number of allocated CPU cores + */ + allocatedCPU: number + /** + * Format: uint32 + * @description Node CPU usage percentage + */ + cpuPercent: number + /** + * Format: uint32 + * @description Total number of CPU cores on the node + */ + cpuCount: number + /** + * Format: uint64 + * @description Amount of allocated memory in bytes + */ + allocatedMemoryBytes: number + /** + * Format: uint64 + * @description Node memory used in bytes + */ + memoryUsedBytes: number + /** + * Format: uint64 + * @description Total node memory in bytes + */ + memoryTotalBytes: number + /** @description Detailed metrics for each disk/mount point */ + disks: components['schemas']['DiskMetrics'][] + } + MachineInfo: { + /** @description CPU family of the node */ + cpuFamily: string + /** @description CPU model of the node */ + cpuModel: string + /** @description CPU model name of the node */ + cpuModelName: string + /** @description CPU architecture of the node */ + cpuArchitecture: string + } + Node: { + /** @description Version of the orchestrator */ + version: string + /** @description Commit of the orchestrator */ + commit: string + /** @description Identifier of the node */ + id: string + /** @description Service instance identifier of the node */ + serviceInstanceID: string + /** @description Identifier of the cluster */ + clusterID: string + machineInfo: components['schemas']['MachineInfo'] + status: components['schemas']['NodeStatus'] + /** + * Format: uint32 + * @description Number of sandboxes running on the node + */ + sandboxCount: number + metrics: components['schemas']['NodeMetrics'] + /** + * Format: uint64 + * @description Number of sandbox create successes + */ + createSuccesses: number + /** + * Format: uint64 + * @description Number of sandbox create fails + */ + createFails: number + /** + * Format: int + * @description Number of starting Sandboxes + */ + sandboxStartingCount: number + } + NodeDetail: { + /** @description Identifier of the cluster */ + clusterID: string + /** @description Version of the orchestrator */ + version: string + /** @description Commit of the orchestrator */ + commit: string + /** @description Identifier of the node */ + id: string + /** @description Service instance identifier of the node */ + serviceInstanceID: string + machineInfo: components['schemas']['MachineInfo'] + status: components['schemas']['NodeStatus'] + /** + * Format: uint32 + * @description Number of sandboxes running on the node + */ + sandboxCount: number + metrics: components['schemas']['NodeMetrics'] + /** @description List of cached builds id on the node */ + cachedBuilds: string[] + /** + * Format: uint64 + * @description Number of sandbox create successes + */ + createSuccesses: number + /** + * Format: uint64 + * @description Number of sandbox create fails + */ + createFails: number + } + CreatedAccessToken: { + /** + * Format: uuid + * @description Identifier of the access token + */ + id: string + /** @description Name of the access token */ + name: string + /** @description The fully created access token */ + token: string + mask: components['schemas']['IdentifierMaskingDetails'] + /** + * Format: date-time + * @description Timestamp of access token creation + */ + createdAt: string + } + NewAccessToken: { + /** @description Name of the access token */ + name: string + } + TeamAPIKey: { + /** + * Format: uuid + * @description Identifier of the API key + */ + id: string + /** @description Name of the API key */ + name: string + mask: components['schemas']['IdentifierMaskingDetails'] + /** + * Format: date-time + * @description Timestamp of API key creation + */ + createdAt: string + createdBy?: components['schemas']['TeamUser'] | null + /** + * Format: date-time + * @description Last time this API key was used + */ + lastUsed?: string | null + } + CreatedTeamAPIKey: { + /** + * Format: uuid + * @description Identifier of the API key + */ + id: string + /** @description Raw value of the API key */ + key: string + mask: components['schemas']['IdentifierMaskingDetails'] + /** @description Name of the API key */ + name: string + /** + * Format: date-time + * @description Timestamp of API key creation + */ + createdAt: string + createdBy?: components['schemas']['TeamUser'] | null + /** + * Format: date-time + * @description Last time this API key was used + */ + lastUsed?: string | null + } + NewTeamAPIKey: { + /** @description Name of the API key */ + name: string + } + UpdateTeamAPIKey: { + /** @description New name for the API key */ + name: string + } + AssignedTemplateTags: { + /** @description Assigned tags of the template */ + tags: string[] + /** + * Format: uuid + * @description Identifier of the build associated with these tags + */ + buildID: string + } + TemplateTag: { + /** @description The tag name */ + tag: string + /** + * Format: uuid + * @description Identifier of the build associated with this tag + */ + buildID: string + /** + * Format: date-time + * @description Time when the tag was assigned + */ + createdAt: string + } + AssignTemplateTagsRequest: { + /** @description Target template in "name:tag" format */ + target: string + /** @description Tags to assign to the template */ + tags: string[] + } + DeleteTemplateTagsRequest: { + /** @description Name of the template */ + name: string + /** @description Tags to delete */ + tags: string[] + } + Error: { + /** + * Format: int32 + * @description Error code + */ + code: number + /** @description Error */ + message: string + } + IdentifierMaskingDetails: { + /** @description Prefix that identifies the token or key type */ + prefix: string + /** @description Length of the token or key */ + valueLength: number + /** @description Prefix used in masked version of the token or key */ + maskedValuePrefix: string + /** @description Suffix used in masked version of the token or key */ + maskedValueSuffix: string + } + Volume: { + /** @description ID of the volume */ + volumeID: string + /** @description Name of the volume */ + name: string + } + VolumeAndToken: { + /** @description ID of the volume */ + volumeID: string + /** @description Name of the volume */ + name: string + /** @description Auth token to use for interacting with volume content */ + token: string + } + NewVolume: { + /** @description Name of the volume */ + name: string + } + } + responses: { + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + } + parameters: { + templateID: string + buildID: string + sandboxID: string + teamID: string + nodeID: string + apiKeyID: string + accessTokenID: string + snapshotID: string + tag: string + /** @description Maximum number of items to return per page */ + paginationLimit: number + /** @description Cursor to start the list from */ + paginationNextToken: string + volumeID: string + } + requestBodies: never + headers: never + pathItems: never } -export type $defs = Record; -export type operations = Record; +export type $defs = Record +export type operations = Record diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index 177464ce9..6bc87c123 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -4,11 +4,11 @@ import { useMutation } from '@tanstack/react-query' import Link from 'next/link' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import type { AddonInfo } from '@/core/domains/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' import { useTRPC } from '@/trpc/client' -import type { AddonInfo } from '@/core/domains/billing/models' import HelpTooltip from '@/ui/help-tooltip' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/billing/select-plan.tsx b/src/features/dashboard/billing/select-plan.tsx index 01546caac..4e2045901 100644 --- a/src/features/dashboard/billing/select-plan.tsx +++ b/src/features/dashboard/billing/select-plan.tsx @@ -2,11 +2,11 @@ import { useMutation } from '@tanstack/react-query' import { useRouter } from 'next/navigation' +import type { TierInfo } from '@/core/domains/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' import { useTRPC } from '@/trpc/client' -import type { TierInfo } from '@/core/domains/billing/models' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' import { diff --git a/src/features/dashboard/billing/types.ts b/src/features/dashboard/billing/types.ts index ae49131ff..0b49e2245 100644 --- a/src/features/dashboard/billing/types.ts +++ b/src/features/dashboard/billing/types.ts @@ -1,5 +1,5 @@ -import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' import type { TeamItems } from '@/core/domains/billing/models' +import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' export interface BillingData { items: TeamItems diff --git a/src/features/dashboard/billing/utils.ts b/src/features/dashboard/billing/utils.ts index 4b60b1385..cbd108f8e 100644 --- a/src/features/dashboard/billing/utils.ts +++ b/src/features/dashboard/billing/utils.ts @@ -1,5 +1,5 @@ -import { l } from '@/lib/clients/logger/logger' import type { TeamItems } from '@/core/domains/billing/models' +import { l } from '@/lib/clients/logger/logger' import { ADDON_500_SANDBOXES_ID, TIER_BASE_ID, TIER_PRO_ID } from './constants' import type { BillingAddonData, BillingTierData } from './types' diff --git a/src/features/dashboard/limits/alert-card.tsx b/src/features/dashboard/limits/alert-card.tsx index dcc164f11..122b85fc8 100644 --- a/src/features/dashboard/limits/alert-card.tsx +++ b/src/features/dashboard/limits/alert-card.tsx @@ -1,7 +1,7 @@ 'use client' -import { useRouteParams } from '@/lib/hooks/use-route-params' import type { BillingLimit } from '@/core/domains/billing/models' +import { useRouteParams } from '@/lib/hooks/use-route-params' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' import LimitForm from './limit-form' diff --git a/src/features/dashboard/limits/limit-card.tsx b/src/features/dashboard/limits/limit-card.tsx index 6bd6b5228..bc4d3c5db 100644 --- a/src/features/dashboard/limits/limit-card.tsx +++ b/src/features/dashboard/limits/limit-card.tsx @@ -1,7 +1,7 @@ 'use client' -import { useRouteParams } from '@/lib/hooks/use-route-params' import type { BillingLimit } from '@/core/domains/billing/models' +import { useRouteParams } from '@/lib/hooks/use-route-params' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' import LimitForm from './limit-form' diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts index f64c276a5..07c20c5ed 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts @@ -1,5 +1,5 @@ -import { formatAxisNumber } from '@/lib/utils/formatting' import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' +import { formatAxisNumber } from '@/lib/utils/formatting' import type { TeamMetricDataPoint } from './types' /** diff --git a/src/features/dashboard/usage/usage-charts-context.tsx b/src/features/dashboard/usage/usage-charts-context.tsx index 36c34c515..11d835be2 100644 --- a/src/features/dashboard/usage/usage-charts-context.tsx +++ b/src/features/dashboard/usage/usage-charts-context.tsx @@ -9,8 +9,8 @@ import { useMemo, useState, } from 'react' -import { fillTimeSeriesWithEmptyPoints } from '@/lib/utils/time-series' import type { UsageResponse } from '@/core/domains/billing/models' +import { fillTimeSeriesWithEmptyPoints } from '@/lib/utils/time-series' import { INITIAL_TIMEFRAME_FALLBACK_RANGE_MS } from './constants' import { calculateTotals, diff --git a/src/lib/hooks/use-team.ts b/src/lib/hooks/use-team.ts index 80c35e101..d7074e63b 100644 --- a/src/lib/hooks/use-team.ts +++ b/src/lib/hooks/use-team.ts @@ -2,8 +2,8 @@ import { useEffect } from 'react' import { useDebounceCallback } from 'usehooks-ts' -import { useDashboard } from '@/features/dashboard/context' import type { ClientTeam } from '@/core/domains/teams/models' +import { useDashboard } from '@/features/dashboard/context' export const useTeamCookieManager = () => { const { team } = useDashboard() diff --git a/src/lib/utils/rewrites.ts b/src/lib/utils/rewrites.ts index c1aaba52d..34220f0ec 100644 --- a/src/lib/utils/rewrites.ts +++ b/src/lib/utils/rewrites.ts @@ -5,8 +5,8 @@ import { type RewriteConfigType, ROUTE_REWRITE_CONFIG, } from '@/configs/rewrites' -import type { RewriteConfig } from '@/types/rewrites.types' import { l } from '@/core/shared/clients/logger/logger' +import type { RewriteConfig } from '@/types/rewrites.types' function getRewriteForPath( path: string, diff --git a/src/lib/utils/server.ts b/src/lib/utils/server.ts index f1780accc..3293b03f9 100644 --- a/src/lib/utils/server.ts +++ b/src/lib/utils/server.ts @@ -5,9 +5,9 @@ import { cache } from 'react' import { z } from 'zod' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' +import { returnServerError } from '@/core/server/actions/utils' import { infra } from '@/core/shared/clients/api' import { l } from '@/core/shared/clients/logger/logger' -import { returnServerError } from '@/core/server/actions/utils' /* * This function generates an e2b user access token for a given user. diff --git a/tsconfig.json b/tsconfig.json index 40c9f36a2..ee11f05e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,12 +26,18 @@ "@/lib/clients/*": ["./src/core/shared/clients/*"], "@/lib/utils/action": ["./src/core/server/actions/utils.ts"], "@/lib/schemas/*": ["./src/core/shared/schemas/*"], - "@/types/argus-api.types": ["./src/core/shared/contracts/argus-api.types.ts"], + "@/types/argus-api.types": [ + "./src/core/shared/contracts/argus-api.types.ts" + ], "@/types/dashboard-api.types": [ "./src/core/shared/contracts/dashboard-api.types.ts" ], - "@/types/infra-api.types": ["./src/core/shared/contracts/infra-api.types.ts"], - "@/types/database.types": ["./src/core/shared/contracts/database.types.ts"], + "@/types/infra-api.types": [ + "./src/core/shared/contracts/infra-api.types.ts" + ], + "@/types/database.types": [ + "./src/core/shared/contracts/database.types.ts" + ], "@/types/errors": ["./src/core/shared/errors.ts"], "@/server/api/errors": ["./src/core/server/adapters/trpc-errors.ts"], "@/server/api/models/builds.models": [ From 1c898b17d675df02941a3c6edbed661428e54018 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 18 Mar 2026 15:28:42 -0700 Subject: [PATCH 08/37] fix: build --- src/core/server/api/middlewares/repository.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/core/server/api/middlewares/repository.ts b/src/core/server/api/middlewares/repository.ts index 8f911345f..7ad1ef3fa 100644 --- a/src/core/server/api/middlewares/repository.ts +++ b/src/core/server/api/middlewares/repository.ts @@ -20,11 +20,22 @@ export function withAuthedRequestRepository< throw unauthorizedUserError() } + if (!ctx.user) { + throw unauthorizedUserError() + } + const repository = createRepository({ accessToken: ctx.session.access_token, }) - return next({ ctx: { ...ctx, ...extendContext(repository) } }) + return next({ + ctx: { + ...ctx, + session: ctx.session, + user: ctx.user, + ...extendContext(repository), + }, + }) }) } @@ -40,6 +51,10 @@ export function withTeamAuthedRequestRepository< throw unauthorizedUserError() } + if (!ctx.user) { + throw unauthorizedUserError() + } + if (!ctx.teamId) { throw forbiddenTeamAccessError() } @@ -49,6 +64,14 @@ export function withTeamAuthedRequestRepository< teamId: ctx.teamId, }) - return next({ ctx: { ...ctx, ...extendContext(repository) } }) + return next({ + ctx: { + ...ctx, + session: ctx.session, + user: ctx.user, + teamId: ctx.teamId, + ...extendContext(repository), + }, + }) }) } From 6ba3ae42bd7ec2978244119bc996daea0491165d Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 19 Mar 2026 13:32:56 -0700 Subject: [PATCH 09/37] feat: include team member providers in table --- src/core/domains/teams/models.ts | 1 + .../domains/teams/teams-repository.server.ts | 17 ++++++ .../dashboard/members/member-table-body.tsx | 4 +- .../dashboard/members/member-table-row.tsx | 55 +++++++++++++++++++ .../dashboard/members/member-table.tsx | 5 +- 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/core/domains/teams/models.ts b/src/core/domains/teams/models.ts index 604d5e4ed..f53883629 100644 --- a/src/core/domains/teams/models.ts +++ b/src/core/domains/teams/models.ts @@ -18,6 +18,7 @@ export type TeamMemberInfo = { email: string name?: string avatar_url?: string + providers?: string[] } export type TeamMemberRelation = { diff --git a/src/core/domains/teams/teams-repository.server.ts b/src/core/domains/teams/teams-repository.server.ts index e49beee95..682030c27 100644 --- a/src/core/domains/teams/teams-repository.server.ts +++ b/src/core/domains/teams/teams-repository.server.ts @@ -1,5 +1,6 @@ import 'server-only' +import type { User } from '@supabase/supabase-js' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import type { components as DashboardComponents } from '@/core/shared/contracts/dashboard-api.types' import { repoErrorFromHttp } from '@/core/shared/errors' @@ -41,6 +42,21 @@ export interface TeamsRepository { ): Promise> } +function extractSignInProviders(user: User | null | undefined): string[] { + const appProviders = Array.isArray(user?.app_metadata?.providers) + ? user.app_metadata.providers.filter( + (provider): provider is string => typeof provider === 'string' + ) + : [] + const identityProviders = + user?.identities + ?.map((identity) => identity.provider) + .filter((provider): provider is string => typeof provider === 'string') ?? + [] + + return [...new Set([...appProviders, ...identityProviders])] +} + export function createTeamsRepository( scope: TeamsRequestScope, deps: TeamsRepositoryDeps = { @@ -112,6 +128,7 @@ export function createTeamsRepository( email: member.email, name: user?.user_metadata?.name, avatar_url: user?.user_metadata?.avatar_url, + providers: extractSignInProviders(user), }, relation: { added_by: member.addedBy ?? null, diff --git a/src/features/dashboard/members/member-table-body.tsx b/src/features/dashboard/members/member-table-body.tsx index aee662333..aacf8ccdf 100644 --- a/src/features/dashboard/members/member-table-body.tsx +++ b/src/features/dashboard/members/member-table-body.tsx @@ -27,7 +27,7 @@ export default async function MemberTableBody({ if (members.length === 0) { return ( - + No Members No team members found. @@ -55,7 +55,7 @@ export default async function MemberTableBody({ } catch (error) { return ( - + provider !== null) ?? [] + return ( @@ -79,6 +116,24 @@ export default function MemberTableRow({ : (member.info.name ?? 'Anonymous')} {member.info.email} + + {providers.length > 0 ? ( +
+ {providers.map(({ key, label, Icon }) => ( + + + {label} + + ))} +
+ ) : ( + - + )} +
{member.relation.added_by === user?.id ? 'You' : (addedByEmail ?? '')} diff --git a/src/features/dashboard/members/member-table.tsx b/src/features/dashboard/members/member-table.tsx index 02bdff36d..912b8208b 100644 --- a/src/features/dashboard/members/member-table.tsx +++ b/src/features/dashboard/members/member-table.tsx @@ -21,12 +21,13 @@ interface MemberTableProps { const MemberTable: FC = ({ params, className }) => { return ( - +
NameE-Mail + ProvidersAdded By @@ -35,7 +36,7 @@ const MemberTable: FC = ({ params, className }) => { - + From 49751c6ebd83e82f8678fc845736384bbc73bd88 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 19 Mar 2026 20:56:37 -0700 Subject: [PATCH 10/37] refactor: update import paths and remove obsolete files - Refactored import paths in the codebase to use the new structure under `src/core/shared` and `src/core/modules`. - Removed obsolete files related to auth and billing domains to streamline the codebase. - Updated `tsconfig.json` to reflect the new paths for contracts and types. - Adjusted test files to align with the new import structure. --- src/__test__/integration/auth.test.ts | 4 +- .../integration/dashboard-route.test.ts | 4 +- .../integration/inspect-sandbox.test.ts | 14 +++---- src/__test__/integration/proxy.test.ts | 2 +- .../integration/resolve-user-team.test.ts | 2 +- src/__test__/setup.ts | 2 +- src/__test__/unit/chart-utils.test.ts | 2 +- .../unit/fill-metrics-with-zeros.test.ts | 2 +- src/__test__/unit/sandbox-lifecycle.test.ts | 2 +- .../sandbox-monitoring-chart-model.test.ts | 2 +- src/app/(auth)/auth/cli/page.tsx | 6 +-- src/app/(auth)/confirm/page.tsx | 2 +- src/app/(rewrites)/[[...slug]]/route.ts | 2 +- src/app/api/auth/callback/route.ts | 4 +- src/app/api/auth/confirm/route.ts | 4 +- src/app/api/auth/email-callback/route.tsx | 2 +- src/app/api/auth/verify-otp/route.ts | 6 +-- src/app/api/health/route.ts | 6 +-- src/app/api/teams/[teamId]/metrics/route.ts | 2 +- src/app/api/teams/[teamId]/metrics/types.ts | 2 +- .../teams/[teamId]/sandboxes/metrics/route.ts | 6 +-- .../teams/[teamId]/sandboxes/metrics/types.ts | 2 +- src/app/api/teams/user/route.ts | 4 +- src/app/api/teams/user/types.ts | 2 +- .../inspect/sandbox/[sandboxId]/route.ts | 10 ++--- src/app/dashboard/[teamIdOrSlug]/layout.tsx | 5 ++- .../dashboard/[teamIdOrSlug]/team-gate.tsx | 5 ++- src/app/dashboard/account/route.ts | 4 +- src/app/dashboard/route.ts | 4 +- src/app/sbx/new/route.ts | 6 +-- src/configs/layout.ts | 2 +- src/configs/mock-data.ts | 2 +- src/core/application/teams/queries.ts | 6 +++ src/core/{domains => modules}/auth/models.ts | 0 .../auth/repository.server.ts | 6 +-- .../{domains => modules}/billing/models.ts | 0 .../billing/repository.server.ts | 2 +- .../{domains => modules}/builds/models.ts | 4 +- .../builds/repository.server.ts | 6 +-- .../keys/repository.server.ts | 2 +- .../sandboxes/models.client.ts | 0 .../{domains => modules}/sandboxes/models.ts | 6 +-- .../sandboxes/repository.server.ts | 10 ++--- .../{domains => modules}/sandboxes/schemas.ts | 0 .../support/repository.server.ts | 4 +- src/core/{domains => modules}/teams/models.ts | 0 .../{domains => modules}/teams/schemas.ts | 2 +- .../teams/teams-repository.server.ts | 4 +- .../teams/user-teams-repository.server.ts | 2 +- .../templates/repository.server.ts | 2 +- .../webhooks/repository.server.ts | 4 +- src/core/server/actions/auth-actions.ts | 8 ++-- src/core/server/actions/key-actions.ts | 8 ++-- src/core/server/actions/sandbox-actions.ts | 8 ++-- src/core/server/actions/team-actions.ts | 17 ++++---- src/core/server/actions/webhooks-actions.ts | 6 +-- src/core/server/adapters/repo-error.ts | 2 +- src/core/server/adapters/trpc-errors.ts | 2 +- src/core/server/api/middlewares/auth.ts | 2 +- src/core/server/api/middlewares/telemetry.ts | 8 ++-- src/core/server/api/routers/billing.ts | 4 +- src/core/server/api/routers/builds.ts | 4 +- src/core/server/api/routers/sandbox.ts | 6 +-- src/core/server/api/routers/sandboxes.ts | 4 +- src/core/server/api/routers/support.ts | 2 +- src/core/server/api/routers/teams.ts | 2 +- src/core/server/api/routers/templates.ts | 2 +- src/core/server/functions/auth/auth.types.ts | 2 +- src/core/server/functions/auth/get-session.ts | 2 +- .../functions/auth/get-user-by-token.ts | 2 +- .../server/functions/auth/validate-email.ts | 2 +- .../server/functions/keys/get-api-keys.ts | 8 ++-- .../sandboxes/get-team-metrics-core.ts | 8 ++-- .../sandboxes/get-team-metrics-max.ts | 8 ++-- .../functions/sandboxes/get-team-metrics.ts | 4 +- src/core/server/functions/sandboxes/utils.ts | 4 +- .../team/get-team-id-from-segment.ts | 6 +-- .../server/functions/team/get-team-limits.ts | 22 ++++++---- .../server/functions/team/get-team-members.ts | 4 +- .../functions/team/resolve-user-team.ts | 6 +-- src/core/server/functions/team/types.ts | 4 +- src/core/server/functions/usage/get-usage.ts | 6 +-- .../server/functions/webhooks/get-webhooks.ts | 8 ++-- src/core/server/functions/webhooks/schema.ts | 2 +- src/core/server/trpc/procedures.ts | 4 +- src/features/dashboard/billing/addons.tsx | 2 +- .../dashboard/billing/select-plan.tsx | 2 +- src/features/dashboard/billing/types.ts | 2 +- src/features/dashboard/billing/utils.ts | 4 +- .../dashboard/build/build-logs-store.ts | 2 +- src/features/dashboard/build/header.tsx | 2 +- src/features/dashboard/build/logs-cells.tsx | 2 +- .../dashboard/build/logs-filter-params.ts | 2 +- src/features/dashboard/build/logs.tsx | 2 +- .../dashboard/build/use-build-logs.ts | 2 +- src/features/dashboard/context.tsx | 2 +- .../layouts/status-indicator.server.tsx | 2 +- src/features/dashboard/limits/alert-card.tsx | 2 +- src/features/dashboard/limits/limit-card.tsx | 2 +- .../navbar/dashboard-survey-popover.tsx | 2 +- src/features/dashboard/sandbox/context.tsx | 2 +- .../dashboard/sandbox/inspect/context.tsx | 2 +- .../dashboard/sandbox/inspect/not-found.tsx | 2 +- .../sandbox/inspect/root-path-input.tsx | 2 +- .../dashboard/sandbox/logs/logs-cells.tsx | 2 +- .../sandbox/logs/logs-filter-params.ts | 2 +- src/features/dashboard/sandbox/logs/logs.tsx | 2 +- .../sandbox/logs/sandbox-logs-store.ts | 2 +- .../use-sandbox-monitoring-controller.ts | 2 +- .../monitoring/utils/chart-lifecycle.ts | 2 +- .../sandbox/monitoring/utils/chart-metrics.ts | 2 +- .../sandbox/monitoring/utils/chart-model.ts | 2 +- .../sandbox/monitoring/utils/timeframe.ts | 2 +- .../sandboxes/list/open-sandbox-dialog.tsx | 2 +- .../sandboxes/list/stores/metrics-store.ts | 2 +- .../sandboxes/live-counter.server.tsx | 2 +- .../charts/team-metrics-chart/types.ts | 2 +- .../charts/team-metrics-chart/utils.ts | 2 +- .../dashboard/settings/general/name-card.tsx | 7 ++++ .../settings/general/profile-picture-card.tsx | 13 ++++-- .../dashboard/settings/webhooks/types.ts | 2 +- src/features/dashboard/sidebar/menu-teams.tsx | 2 +- .../dashboard/templates/builds/constants.ts | 2 +- .../dashboard/templates/builds/header.tsx | 2 +- .../templates/builds/table-cells.tsx | 4 +- .../dashboard/templates/builds/table.tsx | 2 +- .../templates/builds/use-filters.tsx | 2 +- .../dashboard/usage/sampling-utils.ts | 2 +- .../dashboard/usage/usage-charts-context.tsx | 2 +- src/features/general-analytics-collector.tsx | 2 +- src/lib/captcha/turnstile.ts | 2 +- src/lib/hooks/use-team.ts | 2 +- src/types/api.types.ts | 2 +- src/ui/error.tsx | 2 +- tsconfig.json | 41 ++----------------- 135 files changed, 265 insertions(+), 265 deletions(-) create mode 100644 src/core/application/teams/queries.ts rename src/core/{domains => modules}/auth/models.ts (100%) rename src/core/{domains => modules}/auth/repository.server.ts (91%) rename src/core/{domains => modules}/billing/models.ts (100%) rename src/core/{domains => modules}/billing/repository.server.ts (99%) rename src/core/{domains => modules}/builds/models.ts (89%) rename src/core/{domains => modules}/builds/repository.server.ts (98%) rename src/core/{domains => modules}/keys/repository.server.ts (98%) rename src/core/{domains => modules}/sandboxes/models.client.ts (100%) rename src/core/{domains => modules}/sandboxes/models.ts (95%) rename src/core/{domains => modules}/sandboxes/repository.server.ts (97%) rename src/core/{domains => modules}/sandboxes/schemas.ts (100%) rename src/core/{domains => modules}/support/repository.server.ts (98%) rename src/core/{domains => modules}/teams/models.ts (100%) rename src/core/{domains => modules}/teams/schemas.ts (90%) rename src/core/{domains => modules}/teams/teams-repository.server.ts (98%) rename src/core/{domains => modules}/teams/user-teams-repository.server.ts (98%) rename src/core/{domains => modules}/templates/repository.server.ts (98%) rename src/core/{domains => modules}/webhooks/repository.server.ts (96%) diff --git a/src/__test__/integration/auth.test.ts b/src/__test__/integration/auth.test.ts index ff3cdfab3..429963f98 100644 --- a/src/__test__/integration/auth.test.ts +++ b/src/__test__/integration/auth.test.ts @@ -44,11 +44,11 @@ const mockSupabaseClient = { } // Mock dependencies -vi.mock('@/lib/clients/supabase/server', () => ({ +vi.mock('@/core/shared/clients/supabase/server', () => ({ createClient: vi.fn(() => mockSupabaseClient), })) -vi.mock('@/lib/clients/supabase/admin', () => ({ +vi.mock('@/core/shared/clients/supabase/admin', () => ({ supabaseAdmin: { auth: vi.fn(), }, diff --git a/src/__test__/integration/dashboard-route.test.ts b/src/__test__/integration/dashboard-route.test.ts index 9b8c328a8..d3535540c 100644 --- a/src/__test__/integration/dashboard-route.test.ts +++ b/src/__test__/integration/dashboard-route.test.ts @@ -43,11 +43,11 @@ const { mockResolveUserTeam: vi.fn(), })) -vi.mock('@/lib/clients/supabase/server', () => ({ +vi.mock('@/core/shared/clients/supabase/server', () => ({ createClient: vi.fn(() => mockSupabaseClient), })) -vi.mock('@/lib/clients/supabase/admin', () => ({ +vi.mock('@/core/shared/clients/supabase/admin', () => ({ supabaseAdmin: mockSupabaseAdmin, })) diff --git a/src/__test__/integration/inspect-sandbox.test.ts b/src/__test__/integration/inspect-sandbox.test.ts index c45c9981e..45b2a8732 100644 --- a/src/__test__/integration/inspect-sandbox.test.ts +++ b/src/__test__/integration/inspect-sandbox.test.ts @@ -16,30 +16,30 @@ const mockSupabaseClient = { }, } -vi.mock('@/lib/clients/supabase/server', () => ({ +vi.mock('@/core/shared/clients/supabase/server', () => ({ createClient: vi.fn(() => Promise.resolve(mockSupabaseClient)), })) -vi.mock('@/lib/clients/supabase/admin', () => ({ +vi.mock('@/core/shared/clients/supabase/admin', () => ({ supabaseAdmin: { from: vi.fn(), }, })) -vi.mock('@/lib/clients/kv', () => ({ +vi.mock('@/core/shared/clients/kv', () => ({ kv: { get: vi.fn(), set: vi.fn(), }, })) -vi.mock('@/lib/clients/api', () => ({ +vi.mock('@/core/shared/clients/api', () => ({ infra: { GET: vi.fn(), }, })) -vi.mock('@/lib/clients/logger/logger', () => ({ +vi.mock('@/core/shared/clients/logger/logger', () => ({ l: { info: vi.fn(), warn: vi.fn(), @@ -82,8 +82,8 @@ vi.mock('next/server', async () => { }) // Import mocked modules after mock setup -import { infra } from '@/lib/clients/api' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' +import { infra } from '@/core/shared/clients/api' +import { supabaseAdmin } from '@/core/shared/clients/supabase/admin' // ============================================================================ // TEST HELPERS diff --git a/src/__test__/integration/proxy.test.ts b/src/__test__/integration/proxy.test.ts index fa970dff4..baff12441 100644 --- a/src/__test__/integration/proxy.test.ts +++ b/src/__test__/integration/proxy.test.ts @@ -15,7 +15,7 @@ vi.mock('@supabase/ssr', () => ({ })) // mock logger to avoid noise in tests -vi.mock('@/lib/clients/logger/logger', () => ({ +vi.mock('@/core/shared/clients/logger/logger', () => ({ l: { error: vi.fn(), }, diff --git a/src/__test__/integration/resolve-user-team.test.ts b/src/__test__/integration/resolve-user-team.test.ts index bcd146bb6..48ff6e01d 100644 --- a/src/__test__/integration/resolve-user-team.test.ts +++ b/src/__test__/integration/resolve-user-team.test.ts @@ -32,7 +32,7 @@ const { mockSupabaseAdmin, mockCookieStore, mockCheckUserTeamAuth } = mockCheckUserTeamAuth: vi.fn(), })) -vi.mock('@/lib/clients/supabase/admin', () => ({ +vi.mock('@/core/shared/clients/supabase/admin', () => ({ supabaseAdmin: mockSupabaseAdmin, })) diff --git a/src/__test__/setup.ts b/src/__test__/setup.ts index 37226c0a9..551fdefc2 100644 --- a/src/__test__/setup.ts +++ b/src/__test__/setup.ts @@ -10,7 +10,7 @@ vi.mock('server-only', () => ({})) vi.mock('server-cli-only', () => ({})) // default mocks -vi.mock('@/lib/clients/logger', () => ({ +vi.mock('@/core/shared/clients/logger', () => ({ l: { error: console.error, info: console.info, diff --git a/src/__test__/unit/chart-utils.test.ts b/src/__test__/unit/chart-utils.test.ts index 29fb54b55..5b0ad4829 100644 --- a/src/__test__/unit/chart-utils.test.ts +++ b/src/__test__/unit/chart-utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetric } from '@/core/modules/sandboxes/models.client' import { transformMetrics } from '@/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils' import { calculateAxisMax } from '@/lib/utils/chart' diff --git a/src/__test__/unit/fill-metrics-with-zeros.test.ts b/src/__test__/unit/fill-metrics-with-zeros.test.ts index 4ae91dfec..0afce30b5 100644 --- a/src/__test__/unit/fill-metrics-with-zeros.test.ts +++ b/src/__test__/unit/fill-metrics-with-zeros.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetrics } from '@/core/modules/sandboxes/models.client' import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' describe('fillTeamMetricsWithZeros', () => { diff --git a/src/__test__/unit/sandbox-lifecycle.test.ts b/src/__test__/unit/sandbox-lifecycle.test.ts index 4ac095579..3b2099d54 100644 --- a/src/__test__/unit/sandbox-lifecycle.test.ts +++ b/src/__test__/unit/sandbox-lifecycle.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { deriveSandboxLifecycleFromEvents, type SandboxEventModel, -} from '@/core/domains/sandboxes/models' +} from '@/core/modules/sandboxes/models' function createLifecycleEvent( overrides: Partial & Pick diff --git a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts index 17315728d..9a3dc87e6 100644 --- a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts +++ b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import type { SandboxEventModel, SandboxMetric, -} from '@/core/domains/sandboxes/models' +} from '@/core/modules/sandboxes/models' import { buildMonitoringChartModel } from '@/features/dashboard/sandbox/monitoring/utils/chart-model' const baseMetric = { diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 87e007846..ba12c67b0 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -3,9 +3,9 @@ import { redirect } from 'next/navigation' import { Suspense } from 'react' import { serializeError } from 'serialize-error' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { generateE2BUserAccessToken } from '@/lib/utils/server' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 2f0b9e4d4..07cd39f90 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -9,7 +9,7 @@ import { ConfirmEmailInputSchema, type OtpType, OtpTypeSchema, -} from '@/core/domains/auth/models' +} from '@/core/modules/auth/models' import { AuthFormMessage } from '@/features/auth/form-message' import { Button } from '@/ui/primitives/button' diff --git a/src/app/(rewrites)/[[...slug]]/route.ts b/src/app/(rewrites)/[[...slug]]/route.ts index d2115a635..72be75588 100644 --- a/src/app/(rewrites)/[[...slug]]/route.ts +++ b/src/app/(rewrites)/[[...slug]]/route.ts @@ -4,7 +4,7 @@ import { constructSitemap } from '@/app/sitemap' import { ALLOW_SEO_INDEXING } from '@/configs/flags' import { ROUTE_REWRITE_CONFIG } from '@/configs/rewrites' import { BASE_URL } from '@/configs/urls' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { getRewriteForPath, rewriteContentPagesHtml, diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts index 241b2d414..96e88090c 100644 --- a/src/app/api/auth/callback/route.ts +++ b/src/app/api/auth/callback/route.ts @@ -1,8 +1,8 @@ import { redirect } from 'next/navigation' import { serializeError } from 'serialize-error' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' export async function GET(request: Request) { diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index 3f9a7ea03..c622fec9d 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -2,8 +2,8 @@ import { redirect } from 'next/navigation' import type { NextRequest } from 'next/server' import { z } from 'zod' import { AUTH_URLS } from '@/configs/urls' -import { OtpTypeSchema } from '@/core/domains/auth/models' -import { l } from '@/lib/clients/logger/logger' +import { OtpTypeSchema } from '@/core/modules/auth/models' +import { l } from '@/core/shared/clients/logger/logger' import { encodedRedirect, isExternalOrigin } from '@/lib/utils/auth' const confirmSchema = z.object({ diff --git a/src/app/api/auth/email-callback/route.tsx b/src/app/api/auth/email-callback/route.tsx index 3fd46b5b4..24be4c0c3 100644 --- a/src/app/api/auth/email-callback/route.tsx +++ b/src/app/api/auth/email-callback/route.tsx @@ -1,6 +1,6 @@ import { redirect } from 'next/navigation' import { PROTECTED_URLS } from '@/configs/urls' -import { createClient } from '@/lib/clients/supabase/server' +import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' export async function GET(request: Request) { diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 0f15ddc76..590361da0 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -4,9 +4,9 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { ConfirmEmailInputSchema, type OtpType, -} from '@/core/domains/auth/models' -import { authRepository } from '@/domains/auth/repository.server' -import { l } from '@/lib/clients/logger/logger' +} from '@/core/modules/auth/models' +import { authRepository } from '@/core/modules/auth/repository.server' +import { l } from '@/core/shared/clients/logger/logger' import { isExternalOrigin } from '@/lib/utils/auth' /** diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 62c6e686a..239eb982d 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,8 +1,8 @@ import { NextResponse } from 'next/server' import { serializeError } from 'serialize-error' -import { api } from '@/lib/clients/api' -import { kv } from '@/lib/clients/kv' -import { l } from '@/lib/clients/logger/logger' +import { api } from '@/core/shared/clients/api' +import { kv } from '@/core/shared/clients/kv' +import { l } from '@/core/shared/clients/logger/logger' export const maxDuration = 10 diff --git a/src/app/api/teams/[teamId]/metrics/route.ts b/src/app/api/teams/[teamId]/metrics/route.ts index 415ec4003..72ae7ba80 100644 --- a/src/app/api/teams/[teamId]/metrics/route.ts +++ b/src/app/api/teams/[teamId]/metrics/route.ts @@ -3,7 +3,7 @@ import 'server-cli-only' import { serializeError } from 'serialize-error' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { getTeamMetricsCore } from '@/core/server/functions/sandboxes/get-team-metrics-core' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { TeamMetricsRequestSchema, type TeamMetricsResponse } from './types' export async function POST( diff --git a/src/app/api/teams/[teamId]/metrics/types.ts b/src/app/api/teams/[teamId]/metrics/types.ts index 3e354c5ad..d32af3573 100644 --- a/src/app/api/teams/[teamId]/metrics/types.ts +++ b/src/app/api/teams/[teamId]/metrics/types.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetrics } from '@/core/modules/sandboxes/models.client' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' export const TeamMetricsRequestSchema = z diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts b/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts index 384521645..fed710bec 100644 --- a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts +++ b/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts @@ -1,11 +1,11 @@ import 'server-cli-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { handleDefaultInfraError } from '@/core/server/actions/utils' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { transformMetricsToClientMetrics } from '@/core/server/functions/sandboxes/utils' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { handleDefaultInfraError } from '@/lib/utils/action' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' import { MetricsRequestSchema, type MetricsResponse } from './types' export async function POST( diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts b/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts index d86f5170b..a702a303e 100644 --- a/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts +++ b/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { ClientSandboxesMetrics } from '@/core/domains/sandboxes/models.client' +import type { ClientSandboxesMetrics } from '@/core/modules/sandboxes/models.client' export const MetricsRequestSchema = z.object({ sandboxIds: z.array(z.string()).min(1, 'Provide at least one sandbox id'), diff --git a/src/app/api/teams/user/route.ts b/src/app/api/teams/user/route.ts index 83164e8ad..59bff2186 100644 --- a/src/app/api/teams/user/route.ts +++ b/src/app/api/teams/user/route.ts @@ -1,6 +1,6 @@ -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' -import { createClient } from '@/lib/clients/supabase/server' +import { createClient } from '@/core/shared/clients/supabase/server' import type { UserTeamsResponse } from './types' export async function GET() { diff --git a/src/app/api/teams/user/types.ts b/src/app/api/teams/user/types.ts index 327ec7c70..c4c0809d8 100644 --- a/src/app/api/teams/user/types.ts +++ b/src/app/api/teams/user/types.ts @@ -1,3 +1,3 @@ -import type { ClientTeam } from '@/core/domains/teams/models' +import type { ClientTeam } from '@/core/modules/teams/models' export type UserTeamsResponse = { teams: ClientTeam[] } diff --git a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts index a0ded9274..12e939b33 100644 --- a/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts +++ b/src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts @@ -4,11 +4,11 @@ import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { COOKIE_KEYS } from '@/configs/cookies' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' -import { SandboxIdSchema } from '@/lib/schemas/api' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' +import { SandboxIdSchema } from '@/core/shared/schemas/api' import { setTeamCookies } from '@/lib/utils/cookies' export const dynamic = 'force-dynamic' diff --git a/src/app/dashboard/[teamIdOrSlug]/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/layout.tsx index 3ce00093d..ae296f098 100644 --- a/src/app/dashboard/[teamIdOrSlug]/layout.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/layout.tsx @@ -5,6 +5,7 @@ import { DashboardTeamGate } from '@/app/dashboard/[teamIdOrSlug]/team-gate' import { COOKIE_KEYS } from '@/configs/cookies' import { METADATA } from '@/configs/metadata' import { AUTH_URLS } from '@/configs/urls' +import { DASHBOARD_TEAMS_LIST_QUERY_OPTIONS } from '@/core/application/teams/queries' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import DashboardLayoutView from '@/features/dashboard/layouts/layout' @@ -44,7 +45,9 @@ export default async function DashboardLayout({ throw redirect(AUTH_URLS.SIGN_IN) } - prefetch(trpc.teams.list.queryOptions()) + prefetch( + trpc.teams.list.queryOptions(undefined, DASHBOARD_TEAMS_LIST_QUERY_OPTIONS) + ) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx index 73e407b26..ad0a4062e 100644 --- a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx @@ -7,6 +7,7 @@ import { } from '@tanstack/react-query' import { Suspense } from 'react' import { ErrorBoundary } from 'react-error-boundary' +import { DASHBOARD_TEAMS_LIST_QUERY_OPTIONS } from '@/core/application/teams/queries' import { DashboardContextProvider } from '@/features/dashboard/context' import { useTRPC } from '@/trpc/client' import Unauthorized from '../unauthorized' @@ -20,7 +21,9 @@ interface DashboardTeamGateProps { function TeamContent({ teamIdOrSlug, user, children }: DashboardTeamGateProps) { const trpc = useTRPC() - const { data: teams } = useSuspenseQuery(trpc.teams.list.queryOptions()) + const { data: teams } = useSuspenseQuery( + trpc.teams.list.queryOptions(undefined, DASHBOARD_TEAMS_LIST_QUERY_OPTIONS) + ) const team = teams.find( (candidate) => diff --git a/src/app/dashboard/account/route.ts b/src/app/dashboard/account/route.ts index f8a87d0fb..e0be135c4 100644 --- a/src/app/dashboard/account/route.ts +++ b/src/app/dashboard/account/route.ts @@ -1,10 +1,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' -import { createClient } from '@/lib/clients/supabase/server' +import { resolveUserTeam } from '@/core/server/functions/team/resolve-user-team' +import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' -import { resolveUserTeam } from '@/server/team/resolve-user-team' export async function GET(request: NextRequest) { const supabase = await createClient() diff --git a/src/app/dashboard/route.ts b/src/app/dashboard/route.ts index b3c4d8a22..26e522391 100644 --- a/src/app/dashboard/route.ts +++ b/src/app/dashboard/route.ts @@ -1,10 +1,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' -import { createClient } from '@/lib/clients/supabase/server' +import { resolveUserTeam } from '@/core/server/functions/team/resolve-user-team' +import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' -import { resolveUserTeam } from '@/server/team/resolve-user-team' export const TAB_URL_MAP: Record string> = { sandboxes: (teamId) => PROTECTED_URLS.SANDBOXES(teamId), diff --git a/src/app/sbx/new/route.ts b/src/app/sbx/new/route.ts index bd93bb2b1..96e59b8ef 100644 --- a/src/app/sbx/new/route.ts +++ b/src/app/sbx/new/route.ts @@ -3,10 +3,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { serializeError } from 'serialize-error' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' export const GET = async (req: NextRequest) => { try { diff --git a/src/configs/layout.ts b/src/configs/layout.ts index 3f39944e8..3c32893fe 100644 --- a/src/configs/layout.ts +++ b/src/configs/layout.ts @@ -1,5 +1,5 @@ import micromatch from 'micromatch' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { PROTECTED_URLS } from './urls' export interface TitleSegment { diff --git a/src/configs/mock-data.ts b/src/configs/mock-data.ts index 5b3c097c4..60d962e7e 100644 --- a/src/configs/mock-data.ts +++ b/src/configs/mock-data.ts @@ -4,7 +4,7 @@ import type { MetricsResponse } from '@/app/api/teams/[teamId]/sandboxes/metrics import type { ClientSandboxesMetrics, ClientTeamMetrics, -} from '@/core/domains/sandboxes/models.client' +} from '@/core/modules/sandboxes/models.client' import type { DefaultTemplate, Sandbox, diff --git a/src/core/application/teams/queries.ts b/src/core/application/teams/queries.ts new file mode 100644 index 000000000..a67699b1f --- /dev/null +++ b/src/core/application/teams/queries.ts @@ -0,0 +1,6 @@ +export const DASHBOARD_TEAMS_LIST_QUERY_OPTIONS = { + staleTime: 5 * 60 * 1000, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, +} as const diff --git a/src/core/domains/auth/models.ts b/src/core/modules/auth/models.ts similarity index 100% rename from src/core/domains/auth/models.ts rename to src/core/modules/auth/models.ts diff --git a/src/core/domains/auth/repository.server.ts b/src/core/modules/auth/repository.server.ts similarity index 91% rename from src/core/domains/auth/repository.server.ts rename to src/core/modules/auth/repository.server.ts index b70edd42a..9ea532f15 100644 --- a/src/core/domains/auth/repository.server.ts +++ b/src/core/modules/auth/repository.server.ts @@ -1,11 +1,11 @@ import 'server-only' import { serializeError } from 'serialize-error' -import type { OtpType } from '@/core/domains/auth/models' +import type { OtpType } from '@/core/modules/auth/models' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' import { repoErrorFromHttp } from '@/core/shared/errors' import { err, ok, type RepoResult } from '@/core/shared/result' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' interface VerifyOtpResult { userId: string diff --git a/src/core/domains/billing/models.ts b/src/core/modules/billing/models.ts similarity index 100% rename from src/core/domains/billing/models.ts rename to src/core/modules/billing/models.ts diff --git a/src/core/domains/billing/repository.server.ts b/src/core/modules/billing/repository.server.ts similarity index 99% rename from src/core/domains/billing/repository.server.ts rename to src/core/modules/billing/repository.server.ts index 31d389baf..8555bd088 100644 --- a/src/core/domains/billing/repository.server.ts +++ b/src/core/modules/billing/repository.server.ts @@ -10,7 +10,7 @@ import type { PaymentMethodsCustomerSession, TeamItems, UsageResponse, -} from '@/core/domains/billing/models' +} from '@/core/modules/billing/models' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' diff --git a/src/core/domains/builds/models.ts b/src/core/modules/builds/models.ts similarity index 89% rename from src/core/domains/builds/models.ts rename to src/core/modules/builds/models.ts index 032d4e1cf..e94eb62c1 100644 --- a/src/core/domains/builds/models.ts +++ b/src/core/modules/builds/models.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -import type { components as dashboardComponents } from '@/types/dashboard-api.types' -import type { components as infraComponents } from '@/types/infra-api.types' +import type { components as dashboardComponents } from '@/contracts/dashboard-api' +import type { components as infraComponents } from '@/contracts/infra-api' export type BuildStatus = dashboardComponents['schemas']['BuildStatus'] diff --git a/src/core/domains/builds/repository.server.ts b/src/core/modules/builds/repository.server.ts similarity index 98% rename from src/core/domains/builds/repository.server.ts rename to src/core/modules/builds/repository.server.ts index 7fca3b8a8..af05b2b58 100644 --- a/src/core/domains/builds/repository.server.ts +++ b/src/core/modules/builds/repository.server.ts @@ -1,18 +1,18 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { components as InfraComponents } from '@/contracts/infra-api' import type { BuildStatus, ListedBuildModel, RunningBuildStatusModel, -} from '@/core/domains/builds/models' +} from '@/core/modules/builds/models' +import { api, infra } from '@/core/shared/clients/api' import { l } from '@/core/shared/clients/logger/logger' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' import { INITIAL_BUILD_STATUSES } from '@/features/dashboard/templates/builds/constants' -import { api, infra } from '@/lib/clients/api' -import type { components as InfraComponents } from '@/types/infra-api.types' type BuildsRepositoryDeps = { apiClient: typeof api diff --git a/src/core/domains/keys/repository.server.ts b/src/core/modules/keys/repository.server.ts similarity index 98% rename from src/core/domains/keys/repository.server.ts rename to src/core/modules/keys/repository.server.ts index c85a9a26f..17f094eb8 100644 --- a/src/core/domains/keys/repository.server.ts +++ b/src/core/modules/keys/repository.server.ts @@ -1,10 +1,10 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { infra } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { infra } from '@/lib/clients/api' import type { CreatedTeamAPIKey, TeamAPIKey } from '@/types/api.types' type KeysRepositoryDeps = { diff --git a/src/core/domains/sandboxes/models.client.ts b/src/core/modules/sandboxes/models.client.ts similarity index 100% rename from src/core/domains/sandboxes/models.client.ts rename to src/core/modules/sandboxes/models.client.ts diff --git a/src/core/domains/sandboxes/models.ts b/src/core/modules/sandboxes/models.ts similarity index 95% rename from src/core/domains/sandboxes/models.ts rename to src/core/modules/sandboxes/models.ts index 308c4530c..caa4a82b3 100644 --- a/src/core/domains/sandboxes/models.ts +++ b/src/core/modules/sandboxes/models.ts @@ -1,6 +1,6 @@ -import type { components as ArgusComponents } from '@/types/argus-api.types' -import type { components as DashboardComponents } from '@/types/dashboard-api.types' -import type { components as InfraComponents } from '@/types/infra-api.types' +import type { components as ArgusComponents } from '@/contracts/argus-api' +import type { components as DashboardComponents } from '@/contracts/dashboard-api' +import type { components as InfraComponents } from '@/contracts/infra-api' export type SandboxLogLevel = InfraComponents['schemas']['LogLevel'] export type Sandbox = InfraComponents['schemas']['ListedSandbox'] diff --git a/src/core/domains/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts similarity index 97% rename from src/core/domains/sandboxes/repository.server.ts rename to src/core/modules/sandboxes/repository.server.ts index 6ff479ea0..993895b90 100644 --- a/src/core/domains/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -1,19 +1,19 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import type { SandboxEventModel } from '@/core/domains/sandboxes/models' +import type { components as DashboardComponents } from '@/contracts/dashboard-api' +import type { components as InfraComponents } from '@/contracts/infra-api' +import type { SandboxEventModel } from '@/core/modules/sandboxes/models' +import { api, infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { api, infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' import type { Sandboxes, SandboxesMetricsRecord, TeamMetric, } from '@/types/api.types' -import type { components as DashboardComponents } from '@/types/dashboard-api.types' -import type { components as InfraComponents } from '@/types/infra-api.types' type SandboxesRepositoryDeps = { apiClient: typeof api diff --git a/src/core/domains/sandboxes/schemas.ts b/src/core/modules/sandboxes/schemas.ts similarity index 100% rename from src/core/domains/sandboxes/schemas.ts rename to src/core/modules/sandboxes/schemas.ts diff --git a/src/core/domains/support/repository.server.ts b/src/core/modules/support/repository.server.ts similarity index 98% rename from src/core/domains/support/repository.server.ts rename to src/core/modules/support/repository.server.ts index f25e06eed..a0c368c17 100644 --- a/src/core/domains/support/repository.server.ts +++ b/src/core/modules/support/repository.server.ts @@ -1,11 +1,11 @@ import 'server-only' import { AttachmentType, PlainClient } from '@team-plain/typescript-sdk' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { l } from '@/core/shared/clients/logger/logger' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { l } from '@/lib/clients/logger/logger' const MAX_FILE_SIZE = 10 * 1024 * 1024 const MAX_FILES = 5 diff --git a/src/core/domains/teams/models.ts b/src/core/modules/teams/models.ts similarity index 100% rename from src/core/domains/teams/models.ts rename to src/core/modules/teams/models.ts diff --git a/src/core/domains/teams/schemas.ts b/src/core/modules/teams/schemas.ts similarity index 90% rename from src/core/domains/teams/schemas.ts rename to src/core/modules/teams/schemas.ts index 3da295d6c..cce34bf2f 100644 --- a/src/core/domains/teams/schemas.ts +++ b/src/core/modules/teams/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' export { TeamIdOrSlugSchema } diff --git a/src/core/domains/teams/teams-repository.server.ts b/src/core/modules/teams/teams-repository.server.ts similarity index 98% rename from src/core/domains/teams/teams-repository.server.ts rename to src/core/modules/teams/teams-repository.server.ts index 682030c27..605c86343 100644 --- a/src/core/domains/teams/teams-repository.server.ts +++ b/src/core/modules/teams/teams-repository.server.ts @@ -2,12 +2,12 @@ import 'server-only' import type { User } from '@supabase/supabase-js' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/core/shared/clients/api' +import { supabaseAdmin } from '@/core/shared/clients/supabase/admin' import type { components as DashboardComponents } from '@/core/shared/contracts/dashboard-api.types' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { api } from '@/lib/clients/api' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' import type { TeamLimits, TeamMember } from './models' type ApiUserTeam = { diff --git a/src/core/domains/teams/user-teams-repository.server.ts b/src/core/modules/teams/user-teams-repository.server.ts similarity index 98% rename from src/core/domains/teams/user-teams-repository.server.ts rename to src/core/modules/teams/user-teams-repository.server.ts index 78a46a675..19867bce9 100644 --- a/src/core/domains/teams/user-teams-repository.server.ts +++ b/src/core/modules/teams/user-teams-repository.server.ts @@ -2,10 +2,10 @@ import 'server-only' import { secondsInDay } from 'date-fns/constants' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { api } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { RequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { api } from '@/lib/clients/api' import type { ClientTeam, ResolvedTeam } from './models' type ApiUserTeam = { diff --git a/src/core/domains/templates/repository.server.ts b/src/core/modules/templates/repository.server.ts similarity index 98% rename from src/core/domains/templates/repository.server.ts rename to src/core/modules/templates/repository.server.ts index 8a9be16df..0126f98df 100644 --- a/src/core/domains/templates/repository.server.ts +++ b/src/core/modules/templates/repository.server.ts @@ -7,13 +7,13 @@ import { MOCK_DEFAULT_TEMPLATES_DATA, MOCK_TEMPLATES_DATA, } from '@/configs/mock-data' +import { api, infra } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { RequestScope, TeamRequestScope, } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { api, infra } from '@/lib/clients/api' import type { DefaultTemplate, Template } from '@/types/api.types' type TemplatesRepositoryDeps = { diff --git a/src/core/domains/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts similarity index 96% rename from src/core/domains/webhooks/repository.server.ts rename to src/core/modules/webhooks/repository.server.ts index bb17c06bc..f7188d7df 100644 --- a/src/core/domains/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -1,11 +1,11 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { infra } from '@/core/shared/clients/api' +import type { components as ArgusComponents } from '@/core/shared/contracts/argus-api.types' import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import { infra } from '@/lib/clients/api' -import type { components as ArgusComponents } from '@/types/argus-api.types' type WebhooksRepositoryDeps = { infraClient: typeof infra diff --git a/src/core/server/actions/auth-actions.ts b/src/core/server/actions/auth-actions.ts index e30f34b55..84b7353be 100644 --- a/src/core/server/actions/auth-actions.ts +++ b/src/core/server/actions/auth-actions.ts @@ -8,6 +8,7 @@ import { CAPTCHA_REQUIRED_SERVER } from '@/configs/flags' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { USER_MESSAGES } from '@/configs/user-messages' import { actionClient } from '@/core/server/actions/client' +import { returnServerError } from '@/core/server/actions/utils' import { forgotPasswordSchema, signInSchema, @@ -17,11 +18,10 @@ import { shouldWarnAboutAlternateEmail, validateEmail, } from '@/core/server/functions/auth/validate-email' +import { l } from '@/core/shared/clients/logger/logger' +import { createClient } from '@/core/shared/clients/supabase/server' +import { relativeUrlSchema } from '@/core/shared/schemas/url' import { verifyTurnstileToken } from '@/lib/captcha/turnstile' -import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' -import { relativeUrlSchema } from '@/lib/schemas/url' -import { returnServerError } from '@/lib/utils/action' import { encodedRedirect } from '@/lib/utils/auth' async function validateCaptcha(captchaToken: string | undefined) { diff --git a/src/core/server/actions/key-actions.ts b/src/core/server/actions/key-actions.ts index a9072091b..fa8bafe63 100644 --- a/src/core/server/actions/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -3,15 +3,15 @@ import { revalidatePath, updateTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { createKeysRepository } from '@/core/domains/keys/repository.server' +import { createKeysRepository } from '@/core/modules/keys/repository.server' import { authActionClient, withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' +import { returnServerError } from '@/core/server/actions/utils' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const withKeysRepository = withTeamAuthedRequestRepository( createKeysRepository, diff --git a/src/core/server/actions/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts index 1bd283968..18ffcd785 100644 --- a/src/core/server/actions/sandbox-actions.ts +++ b/src/core/server/actions/sandbox-actions.ts @@ -8,10 +8,10 @@ import { authActionClient, withTeamIdResolution, } from '@/core/server/actions/client' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' +import { returnServerError } from '@/core/server/actions/utils' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const KillSandboxSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, diff --git a/src/core/server/actions/team-actions.ts b/src/core/server/actions/team-actions.ts index 33e08057b..0640f710b 100644 --- a/src/core/server/actions/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -8,22 +8,25 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { zfd } from 'zod-form-data' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import type { CreateTeamsResponse } from '@/core/domains/billing/models' +import type { CreateTeamsResponse } from '@/core/modules/billing/models' import { CreateTeamSchema, UpdateTeamNameSchema, -} from '@/core/domains/teams/schemas' -import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +} from '@/core/modules/teams/schemas' +import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server' import { authActionClient, withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' +import { + handleDefaultInfraError, + returnServerError, +} from '@/core/server/actions/utils' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { l } from '@/lib/clients/logger/logger' -import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { handleDefaultInfraError, returnServerError } from '@/lib/utils/action' +import { l } from '@/core/shared/clients/logger/logger' +import { deleteFile, getFiles, uploadFile } from '@/core/shared/clients/storage' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const withTeamsRepository = withTeamAuthedRequestRepository( createTeamsRepository, diff --git a/src/core/server/actions/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts index 8c788af1b..b939ace7b 100644 --- a/src/core/server/actions/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -3,19 +3,19 @@ import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' import { COOKIE_KEYS } from '@/configs/cookies' -import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' +import { createWebhooksRepository } from '@/core/modules/webhooks/repository.server' import { authActionClient, withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' +import { handleDefaultInfraError } from '@/core/server/actions/utils' import { DeleteWebhookSchema, UpdateWebhookSecretSchema, UpsertWebhookSchema, } from '@/core/server/functions/webhooks/schema' -import { l } from '@/lib/clients/logger/logger' -import { handleDefaultInfraError } from '@/lib/utils/action' +import { l } from '@/core/shared/clients/logger/logger' const withWebhooksRepository = withTeamAuthedRequestRepository( createWebhooksRepository, diff --git a/src/core/server/adapters/repo-error.ts b/src/core/server/adapters/repo-error.ts index 0c9134009..0391aae1a 100644 --- a/src/core/server/adapters/repo-error.ts +++ b/src/core/server/adapters/repo-error.ts @@ -1,6 +1,6 @@ import { TRPCError } from '@trpc/server' +import { ActionError } from '@/core/server/actions/utils' import type { RepoError } from '@/core/shared/result' -import { ActionError } from '@/lib/utils/action' function trpcCodeFromRepoError(code: RepoError['code']): TRPCError['code'] { switch (code) { diff --git a/src/core/server/adapters/trpc-errors.ts b/src/core/server/adapters/trpc-errors.ts index 2a4463e91..e0bc64fbb 100644 --- a/src/core/server/adapters/trpc-errors.ts +++ b/src/core/server/adapters/trpc-errors.ts @@ -1,5 +1,5 @@ import { TRPCError } from '@trpc/server' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' export const forbiddenTeamAccessError = () => new TRPCError({ diff --git a/src/core/server/api/middlewares/auth.ts b/src/core/server/api/middlewares/auth.ts index 1ab89d1ac..0228999ee 100644 --- a/src/core/server/api/middlewares/auth.ts +++ b/src/core/server/api/middlewares/auth.ts @@ -8,7 +8,7 @@ import { unauthorizedUserError } from '@/core/server/adapters/trpc-errors' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import { t } from '@/core/server/trpc/init' -import { getTracer } from '@/lib/clients/tracer' +import { getTracer } from '@/core/shared/clients/tracer' const createSupabaseServerClient = (headers: Headers) => { return createServerClient( diff --git a/src/core/server/api/middlewares/telemetry.ts b/src/core/server/api/middlewares/telemetry.ts index 0ac4208e8..f1fc21838 100644 --- a/src/core/server/api/middlewares/telemetry.ts +++ b/src/core/server/api/middlewares/telemetry.ts @@ -9,12 +9,12 @@ import { import type { User } from '@supabase/supabase-js' import { TRPCError } from '@trpc/server' import { serializeError } from 'serialize-error' +import { flattenClientInputValue } from '@/core/server/actions/utils' import { internalServerError } from '@/core/server/adapters/trpc-errors' import { t } from '@/core/server/trpc/init' -import { l } from '@/lib/clients/logger/logger' -import { getMeter } from '@/lib/clients/meter' -import { getTracer } from '@/lib/clients/tracer' -import { flattenClientInputValue } from '@/lib/utils/action' +import { l } from '@/core/shared/clients/logger/logger' +import { getMeter } from '@/core/shared/clients/meter' +import { getTracer } from '@/core/shared/clients/tracer' /** * Telemetry State diff --git a/src/core/server/api/routers/billing.ts b/src/core/server/api/routers/billing.ts index 3bceb7cba..261437e92 100644 --- a/src/core/server/api/routers/billing.ts +++ b/src/core/server/api/routers/billing.ts @@ -1,8 +1,8 @@ import { TRPCError } from '@trpc/server' import { headers } from 'next/headers' import { z } from 'zod' -import { createBillingRepository } from '@/core/domains/billing/repository.server' -import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +import { createBillingRepository } from '@/core/modules/billing/repository.server' +import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' diff --git a/src/core/server/api/routers/builds.ts b/src/core/server/api/routers/builds.ts index a10b1cc38..534c96923 100644 --- a/src/core/server/api/routers/builds.ts +++ b/src/core/server/api/routers/builds.ts @@ -4,8 +4,8 @@ import { type BuildLogModel, type BuildLogsModel, BuildStatusSchema, -} from '@/core/domains/builds/models' -import { createBuildsRepository } from '@/core/domains/builds/repository.server' +} from '@/core/modules/builds/models' +import { createBuildsRepository } from '@/core/modules/builds/repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index a93ef2708..7b4a31e75 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -8,14 +8,14 @@ import { type SandboxDetailsModel, type SandboxLogModel, type SandboxLogsModel, -} from '@/core/domains/sandboxes/models' -import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' +} from '@/core/modules/sandboxes/models' +import { createSandboxesRepository } from '@/core/modules/sandboxes/repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' import { protectedTeamProcedure } from '@/core/server/trpc/procedures' +import { SandboxIdSchema } from '@/core/shared/schemas/api' import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' -import { SandboxIdSchema } from '@/lib/schemas/api' const sandboxRepositoryProcedure = protectedTeamProcedure.use( withTeamAuthedRequestRepository( diff --git a/src/core/server/api/routers/sandboxes.ts b/src/core/server/api/routers/sandboxes.ts index 861eb523a..dd42abd6c 100644 --- a/src/core/server/api/routers/sandboxes.ts +++ b/src/core/server/api/routers/sandboxes.ts @@ -6,11 +6,11 @@ import { MOCK_TEAM_METRICS_DATA, MOCK_TEAM_METRICS_MAX_DATA, } from '@/configs/mock-data' -import { createSandboxesRepository } from '@/core/domains/sandboxes/repository.server' +import { createSandboxesRepository } from '@/core/modules/sandboxes/repository.server' import { GetTeamMetricsMaxSchema, GetTeamMetricsSchema, -} from '@/core/domains/sandboxes/schemas' +} from '@/core/modules/sandboxes/schemas' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { diff --git a/src/core/server/api/routers/support.ts b/src/core/server/api/routers/support.ts index 4b94d72a3..d00ea358e 100644 --- a/src/core/server/api/routers/support.ts +++ b/src/core/server/api/routers/support.ts @@ -1,6 +1,6 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' -import { createSupportRepository } from '@/core/domains/support/repository.server' +import { createSupportRepository } from '@/core/modules/support/repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { createTRPCRouter } from '@/core/server/trpc/init' diff --git a/src/core/server/api/routers/teams.ts b/src/core/server/api/routers/teams.ts index 2f63cad50..60a8c5e36 100644 --- a/src/core/server/api/routers/teams.ts +++ b/src/core/server/api/routers/teams.ts @@ -1,4 +1,4 @@ -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { protectedProcedure } from '@/core/server/trpc/procedures' diff --git a/src/core/server/api/routers/templates.ts b/src/core/server/api/routers/templates.ts index 6d97a4864..2ab4f2757 100644 --- a/src/core/server/api/routers/templates.ts +++ b/src/core/server/api/routers/templates.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { createDefaultTemplatesRepository, createTemplatesRepository, -} from '@/core/domains/templates/repository.server' +} from '@/core/modules/templates/repository.server' import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/repo-error' import { withAuthedRequestRepository, diff --git a/src/core/server/functions/auth/auth.types.ts b/src/core/server/functions/auth/auth.types.ts index 98f0e3a81..42786c86b 100644 --- a/src/core/server/functions/auth/auth.types.ts +++ b/src/core/server/functions/auth/auth.types.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { CAPTCHA_REQUIRED_CLIENT } from '@/configs/flags' -import { relativeUrlSchema } from '@/lib/schemas/url' +import { relativeUrlSchema } from '@/core/shared/schemas/url' export const emailSchema = z.email('Valid email is required') diff --git a/src/core/server/functions/auth/get-session.ts b/src/core/server/functions/auth/get-session.ts index 3623d6459..eac071784 100644 --- a/src/core/server/functions/auth/get-session.ts +++ b/src/core/server/functions/auth/get-session.ts @@ -1,4 +1,4 @@ -import { createClient } from '@/lib/clients/supabase/server' +import { createClient } from '@/core/shared/clients/supabase/server' import 'server-cli-only' /** diff --git a/src/core/server/functions/auth/get-user-by-token.ts b/src/core/server/functions/auth/get-user-by-token.ts index 02d43d4ce..ce24da1a7 100644 --- a/src/core/server/functions/auth/get-user-by-token.ts +++ b/src/core/server/functions/auth/get-user-by-token.ts @@ -2,7 +2,7 @@ import 'server-only' import { AuthSessionMissingError } from '@supabase/supabase-js' import { cache } from 'react' -import { supabaseAdmin } from '@/lib/clients/supabase/admin' +import { supabaseAdmin } from '@/core/shared/clients/supabase/admin' /** * Retrieves a user from Supabase using their access token. diff --git a/src/core/server/functions/auth/validate-email.ts b/src/core/server/functions/auth/validate-email.ts index bf32b9f42..36fefc537 100644 --- a/src/core/server/functions/auth/validate-email.ts +++ b/src/core/server/functions/auth/validate-email.ts @@ -1,7 +1,7 @@ import { kv } from '@vercel/kv' import { serializeError } from 'serialize-error' import { KV_KEYS } from '@/configs/keys' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' /** * Response type from the ZeroBounce email validation API diff --git a/src/core/server/functions/keys/get-api-keys.ts b/src/core/server/functions/keys/get-api-keys.ts index dc857cbdb..acd6ab857 100644 --- a/src/core/server/functions/keys/get-api-keys.ts +++ b/src/core/server/functions/keys/get-api-keys.ts @@ -1,14 +1,14 @@ import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { createKeysRepository } from '@/core/domains/keys/repository.server' +import { createKeysRepository } from '@/core/modules/keys/repository.server' import { authActionClient, withTeamIdResolution, } from '@/core/server/actions/client' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { handleDefaultInfraError } from '@/lib/utils/action' +import { handleDefaultInfraError } from '@/core/server/actions/utils' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const GetApiKeysSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, diff --git a/src/core/server/functions/sandboxes/get-team-metrics-core.ts b/src/core/server/functions/sandboxes/get-team-metrics-core.ts index 939bf4c88..379caaac3 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-core.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-core.ts @@ -7,11 +7,11 @@ import { calculateTeamMetricsStep, MOCK_TEAM_METRICS_DATA, } from '@/configs/mock-data' -import type { ClientTeamMetrics } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetrics } from '@/core/modules/sandboxes/models.client' +import { handleDefaultInfraError } from '@/core/server/actions/utils' import { fillTeamMetricsWithZeros } from '@/core/server/functions/sandboxes/utils' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { handleDefaultInfraError } from '@/lib/utils/action' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' interface GetTeamMetricsCoreParams { accessToken: string diff --git a/src/core/server/functions/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts index d832c1bde..3fee161f8 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-max.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-max.ts @@ -8,11 +8,11 @@ import { authActionClient, withTeamIdResolution, } from '@/core/server/actions/client' +import { handleDefaultInfraError } from '@/core/server/actions/utils' +import { infra } from '@/core/shared/clients/api' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { infra } from '@/lib/clients/api' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { handleDefaultInfraError } from '@/lib/utils/action' export const GetTeamMetricsMaxSchema = z .object({ diff --git a/src/core/server/functions/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts index 5d2ea5607..51d066605 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics.ts @@ -5,9 +5,9 @@ import { authActionClient, withTeamIdResolution, } from '@/core/server/actions/client' +import { returnServerError } from '@/core/server/actions/utils' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' import { getTeamMetricsCore } from './get-team-metrics-core' export const GetTeamMetricsSchema = z diff --git a/src/core/server/functions/sandboxes/utils.ts b/src/core/server/functions/sandboxes/utils.ts index e2546a87c..4d0d2fda0 100644 --- a/src/core/server/functions/sandboxes/utils.ts +++ b/src/core/server/functions/sandboxes/utils.ts @@ -1,9 +1,9 @@ import { TEAM_METRICS_BACKEND_COLLECTION_INTERVAL_MS } from '@/configs/intervals' -import type { SandboxesMetricsRecord } from '@/core/domains/sandboxes/models' +import type { SandboxesMetricsRecord } from '@/core/modules/sandboxes/models' import type { ClientSandboxesMetrics, ClientTeamMetrics, -} from '@/core/domains/sandboxes/models.client' +} from '@/core/modules/sandboxes/models.client' export function transformMetricsToClientMetrics( metrics: SandboxesMetricsRecord diff --git a/src/core/server/functions/team/get-team-id-from-segment.ts b/src/core/server/functions/team/get-team-id-from-segment.ts index c9dfffcd0..f943654a4 100644 --- a/src/core/server/functions/team/get-team-id-from-segment.ts +++ b/src/core/server/functions/team/get-team-id-from-segment.ts @@ -2,9 +2,9 @@ import 'server-only' import z from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' export const getTeamIdFromSegment = async ( segment: string, diff --git a/src/core/server/functions/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts index 9327d0d60..fdf4c9dcd 100644 --- a/src/core/server/functions/team/get-team-limits.ts +++ b/src/core/server/functions/team/get-team-limits.ts @@ -1,15 +1,15 @@ import 'server-only' +import { cache } from 'react' import { z } from 'zod' import { USE_MOCK_DATA } from '@/configs/flags' -import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server' import { authActionClient, - withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' export interface TeamLimits { concurrentInstances: number @@ -31,22 +31,28 @@ const GetTeamLimitsSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, }) -const withTeamsRepository = withTeamAuthedRequestRepository( - createTeamsRepository, - (teamsRepository) => ({ teamsRepository }) +const getTeamLimitsCached = cache( + async (accessToken: string, teamId: string) => { + return createTeamsRepository({ + accessToken, + teamId, + }).getTeamLimits() + } ) export const getTeamLimits = authActionClient .schema(GetTeamLimitsSchema) .metadata({ serverFunctionName: 'getTeamLimits' }) .use(withTeamIdResolution) - .use(withTeamsRepository) .action(async ({ ctx }) => { if (USE_MOCK_DATA) { return MOCK_TIER_LIMITS } - const limitsResult = await ctx.teamsRepository.getTeamLimits() + const limitsResult = await getTeamLimitsCached( + ctx.session.access_token, + ctx.teamId + ) if (!limitsResult.ok) { return toActionErrorFromRepoError(limitsResult.error) diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts index 67273a708..e77fce38e 100644 --- a/src/core/server/functions/team/get-team-members.ts +++ b/src/core/server/functions/team/get-team-members.ts @@ -1,14 +1,14 @@ import 'server-only' import { z } from 'zod' -import { createTeamsRepository } from '@/core/domains/teams/teams-repository.server' +import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server' import { authActionClient, withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const withTeamsRepository = withTeamAuthedRequestRepository( createTeamsRepository, diff --git a/src/core/server/functions/team/resolve-user-team.ts b/src/core/server/functions/team/resolve-user-team.ts index 8f5596317..5da43f5b8 100644 --- a/src/core/server/functions/team/resolve-user-team.ts +++ b/src/core/server/functions/team/resolve-user-team.ts @@ -2,9 +2,9 @@ import 'server-only' import { cookies } from 'next/headers' import { COOKIE_KEYS } from '@/configs/cookies' -import type { ResolvedTeam } from '@/core/domains/teams/models' -import { createUserTeamsRepository } from '@/core/domains/teams/user-teams-repository.server' -import { l } from '@/lib/clients/logger/logger' +import type { ResolvedTeam } from '@/core/modules/teams/models' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { l } from '@/core/shared/clients/logger/logger' export async function resolveUserTeam( accessToken: string diff --git a/src/core/server/functions/team/types.ts b/src/core/server/functions/team/types.ts index 3ac473e17..616cb87a9 100644 --- a/src/core/server/functions/team/types.ts +++ b/src/core/server/functions/team/types.ts @@ -3,9 +3,9 @@ export type { TeamMember, TeamMemberInfo, TeamMemberRelation, -} from '@/core/domains/teams/models' +} from '@/core/modules/teams/models' export { CreateTeamSchema, TeamNameSchema, UpdateTeamNameSchema, -} from '@/core/domains/teams/schemas' +} from '@/core/modules/teams/schemas' diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts index f9be4711a..7377e0faa 100644 --- a/src/core/server/functions/usage/get-usage.ts +++ b/src/core/server/functions/usage/get-usage.ts @@ -3,13 +3,13 @@ import 'server-only' import { cacheLife, cacheTag } from 'next/cache' import { z } from 'zod' import { CACHE_TAGS } from '@/configs/cache' -import { createBillingRepository } from '@/core/domains/billing/repository.server' +import { createBillingRepository } from '@/core/modules/billing/repository.server' import { authActionClient, withTeamIdResolution, } from '@/core/server/actions/client' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { returnServerError } from '@/lib/utils/action' +import { returnServerError } from '@/core/server/actions/utils' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const GetUsageAuthActionSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, diff --git a/src/core/server/functions/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts index a59311563..78df2d835 100644 --- a/src/core/server/functions/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -1,15 +1,15 @@ import 'server-only' import { z } from 'zod' -import { createWebhooksRepository } from '@/core/domains/webhooks/repository.server' +import { createWebhooksRepository } from '@/core/modules/webhooks/repository.server' import { authActionClient, withTeamAuthedRequestRepository, withTeamIdResolution, } from '@/core/server/actions/client' -import { l } from '@/lib/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' -import { handleDefaultInfraError } from '@/lib/utils/action' +import { handleDefaultInfraError } from '@/core/server/actions/utils' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const GetWebhooksSchema = z.object({ teamIdOrSlug: TeamIdOrSlugSchema, diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index eda667cb7..bfcfe13e7 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' const WebhookUrlSchema = z.httpUrl('Must be a valid URL').trim() const WebhookSecretSchema = z diff --git a/src/core/server/trpc/procedures.ts b/src/core/server/trpc/procedures.ts index d9f015949..4fc5950b2 100644 --- a/src/core/server/trpc/procedures.ts +++ b/src/core/server/trpc/procedures.ts @@ -7,8 +7,8 @@ import { startTelemetryMiddleware, } from '@/core/server/api/middlewares/telemetry' import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' -import { getTracer } from '@/lib/clients/tracer' -import { TeamIdOrSlugSchema } from '@/lib/schemas/team' +import { getTracer } from '@/core/shared/clients/tracer' +import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' import { t } from './init' /** diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index 6bc87c123..2d7266b41 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -4,7 +4,7 @@ import { useMutation } from '@tanstack/react-query' import Link from 'next/link' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' -import type { AddonInfo } from '@/core/domains/billing/models' +import type { AddonInfo } from '@/core/modules/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' diff --git a/src/features/dashboard/billing/select-plan.tsx b/src/features/dashboard/billing/select-plan.tsx index 4e2045901..24d1bf180 100644 --- a/src/features/dashboard/billing/select-plan.tsx +++ b/src/features/dashboard/billing/select-plan.tsx @@ -2,7 +2,7 @@ import { useMutation } from '@tanstack/react-query' import { useRouter } from 'next/navigation' -import type { TierInfo } from '@/core/domains/billing/models' +import type { TierInfo } from '@/core/modules/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' diff --git a/src/features/dashboard/billing/types.ts b/src/features/dashboard/billing/types.ts index 0b49e2245..d4383cf7f 100644 --- a/src/features/dashboard/billing/types.ts +++ b/src/features/dashboard/billing/types.ts @@ -1,4 +1,4 @@ -import type { TeamItems } from '@/core/domains/billing/models' +import type { TeamItems } from '@/core/modules/billing/models' import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' export interface BillingData { diff --git a/src/features/dashboard/billing/utils.ts b/src/features/dashboard/billing/utils.ts index cbd108f8e..a0aaa4bed 100644 --- a/src/features/dashboard/billing/utils.ts +++ b/src/features/dashboard/billing/utils.ts @@ -1,5 +1,5 @@ -import type { TeamItems } from '@/core/domains/billing/models' -import { l } from '@/lib/clients/logger/logger' +import type { TeamItems } from '@/core/modules/billing/models' +import { l } from '@/core/shared/clients/logger/logger' import { ADDON_500_SANDBOXES_ID, TIER_BASE_ID, TIER_PRO_ID } from './constants' import type { BillingAddonData, BillingTierData } from './types' diff --git a/src/features/dashboard/build/build-logs-store.ts b/src/features/dashboard/build/build-logs-store.ts index 763ec0f97..26b9de7f3 100644 --- a/src/features/dashboard/build/build-logs-store.ts +++ b/src/features/dashboard/build/build-logs-store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' -import type { BuildLogModel } from '@/core/domains/builds/models' +import type { BuildLogModel } from '@/core/modules/builds/models' import type { useTRPCClient } from '@/trpc/client' import { countLeadingAtTimestamp, diff --git a/src/features/dashboard/build/header.tsx b/src/features/dashboard/build/header.tsx index cb5c4ecc9..48bba80f2 100644 --- a/src/features/dashboard/build/header.tsx +++ b/src/features/dashboard/build/header.tsx @@ -1,6 +1,6 @@ 'use client' -import type { BuildDetailsModel } from '@/core/domains/builds/models' +import type { BuildDetailsModel } from '@/core/modules/builds/models' import { cn } from '@/lib/utils/ui' import CopyButton from '@/ui/copy-button' import CopyButtonInline from '@/ui/copy-button-inline' diff --git a/src/features/dashboard/build/logs-cells.tsx b/src/features/dashboard/build/logs-cells.tsx index f8a46afc2..620f1709c 100644 --- a/src/features/dashboard/build/logs-cells.tsx +++ b/src/features/dashboard/build/logs-cells.tsx @@ -1,6 +1,6 @@ import { format } from 'date-fns' import { enUS } from 'date-fns/locale/en-US' -import type { BuildLogModel } from '@/core/domains/builds/models' +import type { BuildLogModel } from '@/core/modules/builds/models' import { LogLevelBadge, LogMessage, diff --git a/src/features/dashboard/build/logs-filter-params.ts b/src/features/dashboard/build/logs-filter-params.ts index 61edab567..802ccfa1e 100644 --- a/src/features/dashboard/build/logs-filter-params.ts +++ b/src/features/dashboard/build/logs-filter-params.ts @@ -1,5 +1,5 @@ import { createLoader, parseAsStringEnum } from 'nuqs/server' -import type { BuildLogModel } from '@/core/domains/builds/models' +import type { BuildLogModel } from '@/core/modules/builds/models' export type LogLevelFilter = BuildLogModel['level'] diff --git a/src/features/dashboard/build/logs.tsx b/src/features/dashboard/build/logs.tsx index 934741f4b..476386c7e 100644 --- a/src/features/dashboard/build/logs.tsx +++ b/src/features/dashboard/build/logs.tsx @@ -9,7 +9,7 @@ import { type RefObject, useCallback, useEffect, useRef, useState } from 'react' import type { BuildDetailsModel, BuildLogModel, -} from '@/core/domains/builds/models' +} from '@/core/modules/builds/models' import { LOG_LEVEL_LEFT_BORDER_CLASS, type LogLevelValue, diff --git a/src/features/dashboard/build/use-build-logs.ts b/src/features/dashboard/build/use-build-logs.ts index 9f6316894..5fe09c48b 100644 --- a/src/features/dashboard/build/use-build-logs.ts +++ b/src/features/dashboard/build/use-build-logs.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useRef } from 'react' import { useStore } from 'zustand' -import type { BuildStatus } from '@/core/domains/builds/models' +import type { BuildStatus } from '@/core/modules/builds/models' import { useTRPCClient } from '@/trpc/client' import { type BuildLogsStore, createBuildLogsStore } from './build-logs-store' import type { LogLevelFilter } from './logs-filter-params' diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index 918094e04..e341710b9 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -2,7 +2,7 @@ import type { User } from '@supabase/supabase-js' import { createContext, type ReactNode, useContext } from 'react' -import type { ClientTeam } from '@/core/domains/teams/models' +import type { ClientTeam } from '@/core/modules/teams/models' interface DashboardContextValue { team: ClientTeam diff --git a/src/features/dashboard/layouts/status-indicator.server.tsx b/src/features/dashboard/layouts/status-indicator.server.tsx index e5f0c1bc7..1eed06e30 100644 --- a/src/features/dashboard/layouts/status-indicator.server.tsx +++ b/src/features/dashboard/layouts/status-indicator.server.tsx @@ -2,7 +2,7 @@ import 'server-only' import { cacheLife } from 'next/cache' import Link from 'next/link' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { LiveDot } from '@/ui/live' const STATUS_PAGE_URL = 'https://status.e2b.dev' diff --git a/src/features/dashboard/limits/alert-card.tsx b/src/features/dashboard/limits/alert-card.tsx index 122b85fc8..24907b1c7 100644 --- a/src/features/dashboard/limits/alert-card.tsx +++ b/src/features/dashboard/limits/alert-card.tsx @@ -1,6 +1,6 @@ 'use client' -import type { BillingLimit } from '@/core/domains/billing/models' +import type { BillingLimit } from '@/core/modules/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' diff --git a/src/features/dashboard/limits/limit-card.tsx b/src/features/dashboard/limits/limit-card.tsx index bc4d3c5db..810744fef 100644 --- a/src/features/dashboard/limits/limit-card.tsx +++ b/src/features/dashboard/limits/limit-card.tsx @@ -1,6 +1,6 @@ 'use client' -import type { BillingLimit } from '@/core/domains/billing/models' +import type { BillingLimit } from '@/core/modules/billing/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' import { useDashboard } from '../context' diff --git a/src/features/dashboard/navbar/dashboard-survey-popover.tsx b/src/features/dashboard/navbar/dashboard-survey-popover.tsx index 29cc269ac..d1992d1f4 100644 --- a/src/features/dashboard/navbar/dashboard-survey-popover.tsx +++ b/src/features/dashboard/navbar/dashboard-survey-popover.tsx @@ -5,8 +5,8 @@ import { usePostHog } from 'posthog-js/react' import { useCallback, useState } from 'react' import { toast } from 'sonner' import { INCLUDE_DASHBOARD_FEEDBACK_SURVEY } from '@/configs/flags' +import { l } from '@/core/shared/clients/logger/logger' import { useAppPostHogProvider } from '@/features/posthog-provider' -import { l } from '@/lib/clients/logger/logger' import { Popover, PopoverContent } from '@/ui/primitives/popover' import { SurveyContent } from '@/ui/survey' diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index a4f41a3ab..d843bf955 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -7,7 +7,7 @@ import { SANDBOXES_METRICS_POLLING_MS } from '@/configs/intervals' import type { SandboxDetailsModel, SandboxEventModel, -} from '@/core/domains/sandboxes/models' +} from '@/core/modules/sandboxes/models' import { useAlignedRefetchInterval } from '@/lib/hooks/use-aligned-refetch-interval' import { useRouteParams } from '@/lib/hooks/use-route-params' import { isNotFoundError } from '@/lib/utils/trpc-errors' diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index d9dd66414..e15acc0d7 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -13,7 +13,7 @@ import { } from 'react' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { AUTH_URLS } from '@/configs/urls' -import { supabase } from '@/lib/clients/supabase/client' +import { supabase } from '@/core/shared/clients/supabase/client' import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' import { useDashboard } from '../../context' diff --git a/src/features/dashboard/sandbox/inspect/not-found.tsx b/src/features/dashboard/sandbox/inspect/not-found.tsx index 55acaa472..448d06131 100644 --- a/src/features/dashboard/sandbox/inspect/not-found.tsx +++ b/src/features/dashboard/sandbox/inspect/not-found.tsx @@ -5,7 +5,7 @@ import { useParams, useRouter } from 'next/navigation' import { useCallback, useEffect, useState, useTransition } from 'react' import { serializeError } from 'serialize-error' import { PROTECTED_URLS } from '@/configs/urls' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/sandbox/inspect/root-path-input.tsx b/src/features/dashboard/sandbox/inspect/root-path-input.tsx index 54739602d..c441966ba 100644 --- a/src/features/dashboard/sandbox/inspect/root-path-input.tsx +++ b/src/features/dashboard/sandbox/inspect/root-path-input.tsx @@ -4,7 +4,7 @@ import { ArrowRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useEffect, useState, useTransition } from 'react' import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/sandbox/logs/logs-cells.tsx b/src/features/dashboard/sandbox/logs/logs-cells.tsx index f5a2739bf..15c138a55 100644 --- a/src/features/dashboard/sandbox/logs/logs-cells.tsx +++ b/src/features/dashboard/sandbox/logs/logs-cells.tsx @@ -1,4 +1,4 @@ -import type { SandboxLogModel } from '@/core/domains/sandboxes/models' +import type { SandboxLogModel } from '@/core/modules/sandboxes/models' import { LogLevelBadge } from '@/features/dashboard/common/log-cells' import CopyButtonInline from '@/ui/copy-button-inline' diff --git a/src/features/dashboard/sandbox/logs/logs-filter-params.ts b/src/features/dashboard/sandbox/logs/logs-filter-params.ts index 8aa4215f6..38138f4ca 100644 --- a/src/features/dashboard/sandbox/logs/logs-filter-params.ts +++ b/src/features/dashboard/sandbox/logs/logs-filter-params.ts @@ -1,5 +1,5 @@ import { createLoader, parseAsString, parseAsStringEnum } from 'nuqs/server' -import type { SandboxLogModel } from '@/core/domains/sandboxes/models' +import type { SandboxLogModel } from '@/core/modules/sandboxes/models' export type LogLevelFilter = SandboxLogModel['level'] diff --git a/src/features/dashboard/sandbox/logs/logs.tsx b/src/features/dashboard/sandbox/logs/logs.tsx index 163d4b84c..d0c12137e 100644 --- a/src/features/dashboard/sandbox/logs/logs.tsx +++ b/src/features/dashboard/sandbox/logs/logs.tsx @@ -13,7 +13,7 @@ import { useState, } from 'react' import { LOG_RETENTION_MS } from '@/configs/logs' -import type { SandboxLogModel } from '@/core/domains/sandboxes/models' +import type { SandboxLogModel } from '@/core/modules/sandboxes/models' import { LOG_LEVEL_LEFT_BORDER_CLASS, type LogLevelValue, diff --git a/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts b/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts index 8028885ee..f64a538b3 100644 --- a/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts +++ b/src/features/dashboard/sandbox/logs/sandbox-logs-store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' -import type { SandboxLogModel } from '@/core/domains/sandboxes/models' +import type { SandboxLogModel } from '@/core/modules/sandboxes/models' import type { useTRPCClient } from '@/trpc/client' import { countLeadingAtTimestamp, diff --git a/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts b/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts index 3a1cc521b..f48838af9 100644 --- a/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts +++ b/src/features/dashboard/sandbox/monitoring/state/use-sandbox-monitoring-controller.ts @@ -3,7 +3,7 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query' import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs' import { useCallback, useEffect, useMemo, useRef } from 'react' -import type { SandboxMetric } from '@/core/domains/sandboxes/models' +import type { SandboxMetric } from '@/core/modules/sandboxes/models' import { useDashboard } from '@/features/dashboard/context' import { useSandboxContext } from '@/features/dashboard/sandbox/context' import { getMsUntilNextAlignedInterval } from '@/lib/hooks/use-aligned-refetch-interval' diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts index e99b48f5e..a07fadfbe 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-lifecycle.ts @@ -1,4 +1,4 @@ -import type { SandboxEventModel } from '@/core/domains/sandboxes/models' +import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import type { SandboxMetricsDataPoint, SandboxMetricsLifecycleEventMarker, diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts index 69f762a6c..152b5686b 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-metrics.ts @@ -1,5 +1,5 @@ import { millisecondsInSecond } from 'date-fns/constants' -import type { SandboxMetric } from '@/core/domains/sandboxes/models' +import type { SandboxMetric } from '@/core/modules/sandboxes/models' import type { SandboxMetricsDataPoint, SandboxMetricsSeries, diff --git a/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts b/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts index 3d6590770..69b619bbc 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/chart-model.ts @@ -1,7 +1,7 @@ import type { SandboxEventModel, SandboxMetric, -} from '@/core/domains/sandboxes/models' +} from '@/core/modules/sandboxes/models' import type { MonitoringChartModel } from '../types/sandbox-metrics-chart' import { applyPauseWindows, diff --git a/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts b/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts index 57aaa11f7..713c76c30 100644 --- a/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts +++ b/src/features/dashboard/sandbox/monitoring/utils/timeframe.ts @@ -1,4 +1,4 @@ -import type { SandboxDetailsModel } from '@/core/domains/sandboxes/models' +import type { SandboxDetailsModel } from '@/core/modules/sandboxes/models' import { calculateStepForDuration } from '@/features/dashboard/sandboxes/monitoring/utils' import { SANDBOX_MONITORING_DEFAULT_RANGE_MS, diff --git a/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx b/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx index e0ef97e39..93f258942 100644 --- a/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx +++ b/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx @@ -4,8 +4,8 @@ import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' import { type FormEvent, useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import { SandboxIdSchema } from '@/core/shared/schemas/api' import { useRouteParams } from '@/lib/hooks/use-route-params' -import { SandboxIdSchema } from '@/lib/schemas/api' import { isNotFoundError } from '@/lib/utils/trpc-errors' import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' diff --git a/src/features/dashboard/sandboxes/list/stores/metrics-store.ts b/src/features/dashboard/sandboxes/list/stores/metrics-store.ts index ea605a401..a7ed7fdbd 100644 --- a/src/features/dashboard/sandboxes/list/stores/metrics-store.ts +++ b/src/features/dashboard/sandboxes/list/stores/metrics-store.ts @@ -1,7 +1,7 @@ 'use client' import { create } from 'zustand' -import type { ClientSandboxesMetrics } from '@/core/domains/sandboxes/models.client' +import type { ClientSandboxesMetrics } from '@/core/modules/sandboxes/models.client' // maximum number of sandbox metrics to keep in memory // this is to prevent the store from growing too large and causing performance issues diff --git a/src/features/dashboard/sandboxes/live-counter.server.tsx b/src/features/dashboard/sandboxes/live-counter.server.tsx index 86827a0ef..a1a90148f 100644 --- a/src/features/dashboard/sandboxes/live-counter.server.tsx +++ b/src/features/dashboard/sandboxes/live-counter.server.tsx @@ -1,6 +1,6 @@ import { Suspense } from 'react' import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { cn } from '@/lib/utils' import { getNowMemo } from '@/lib/utils/server' import { Skeleton } from '@/ui/primitives/skeleton' diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts index 21a426074..c1f649c55 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/types.ts @@ -1,4 +1,4 @@ -import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetric } from '@/core/modules/sandboxes/models.client' export type ChartType = 'concurrent' | 'start-rate' diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts index 07c20c5ed..8a1d9e6ab 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/utils.ts @@ -1,4 +1,4 @@ -import type { ClientTeamMetric } from '@/core/domains/sandboxes/models.client' +import type { ClientTeamMetric } from '@/core/modules/sandboxes/models.client' import { formatAxisNumber } from '@/lib/utils/formatting' import type { TeamMetricDataPoint } from './types' diff --git a/src/features/dashboard/settings/general/name-card.tsx b/src/features/dashboard/settings/general/name-card.tsx index 1dc139e63..8c5fdba3a 100644 --- a/src/features/dashboard/settings/general/name-card.tsx +++ b/src/features/dashboard/settings/general/name-card.tsx @@ -2,6 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormOptimisticAction } from '@next-safe-action/adapter-react-hook-form/hooks' +import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'motion/react' import { USER_MESSAGES } from '@/configs/user-messages' import { updateTeamNameAction } from '@/core/server/actions/team-actions' @@ -13,6 +14,7 @@ import { useToast, } from '@/lib/hooks/use-toast' import { exponentialSmoothing } from '@/lib/utils' +import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' import { Card, @@ -38,6 +40,8 @@ export function NameCard({ className }: NameCardProps) { 'use no memo' const { team } = useDashboard() + const trpc = useTRPC() + const queryClient = useQueryClient() const { toast } = useToast() @@ -70,6 +74,9 @@ export function NameCard({ className }: NameCardProps) { } }, onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: trpc.teams.list.queryKey(), + }) toast(defaultSuccessToast(USER_MESSAGES.teamNameUpdated.message)) }, onError: ({ error }) => { diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index cd1f41829..879374ed8 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -1,6 +1,7 @@ 'use client' import { AnimatePresence, motion } from 'framer-motion' +import { useQueryClient } from '@tanstack/react-query' import { ChevronsUp, ImagePlusIcon, Loader2, Pencil } from 'lucide-react' import { useAction } from 'next-safe-action/hooks' import { useRef, useState } from 'react' @@ -13,6 +14,7 @@ import { useToast, } from '@/lib/hooks/use-toast' import { cn, exponentialSmoothing } from '@/lib/utils' +import { useTRPC } from '@/trpc/client' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { Badge } from '@/ui/primitives/badge' import { cardVariants } from '@/ui/primitives/card' @@ -23,6 +25,8 @@ interface ProfilePictureCardProps { export function ProfilePictureCard({ className }: ProfilePictureCardProps) { const { team } = useDashboard() + const trpc = useTRPC() + const queryClient = useQueryClient() const { toast } = useToast() const fileInputRef = useRef(null) const [isHovered, setIsHovered] = useState(false) @@ -30,7 +34,10 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { const { execute: uploadProfilePicture, isExecuting: isUploading } = useAction( uploadTeamProfilePictureAction, { - onSuccess: () => { + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: trpc.teams.list.queryKey(), + }) toast(defaultSuccessToast(USER_MESSAGES.teamLogoUpdated.message)) }, onError: ({ error }) => { @@ -54,8 +61,8 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { ) const handleUpload = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - const file = e.target.files[0] + const file = e.target.files?.[0] + if (file) { const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB in bytes diff --git a/src/features/dashboard/settings/webhooks/types.ts b/src/features/dashboard/settings/webhooks/types.ts index 974a80788..4a1ed63e4 100644 --- a/src/features/dashboard/settings/webhooks/types.ts +++ b/src/features/dashboard/settings/webhooks/types.ts @@ -1,3 +1,3 @@ -import type { components as ArgusComponents } from '@/types/argus-api.types' +import type { components as ArgusComponents } from '@/contracts/argus-api' export type Webhook = ArgusComponents['schemas']['WebhookDetail'] diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index dcea352be..2c0949beb 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' import { usePathname, useSearchParams } from 'next/navigation' import { useCallback } from 'react' -import type { ClientTeam } from '@/core/domains/teams/models' +import type { ClientTeam } from '@/core/modules/teams/models' import { useTeamCookieManager } from '@/lib/hooks/use-team' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { diff --git a/src/features/dashboard/templates/builds/constants.ts b/src/features/dashboard/templates/builds/constants.ts index eb98a9d63..37e090585 100644 --- a/src/features/dashboard/templates/builds/constants.ts +++ b/src/features/dashboard/templates/builds/constants.ts @@ -1,5 +1,5 @@ import { millisecondsInDay } from 'date-fns/constants' -import type { BuildStatus } from '@/core/domains/builds/models' +import type { BuildStatus } from '@/core/modules/builds/models' export const LOG_RETENTION_MS = 7 * millisecondsInDay // 7 days diff --git a/src/features/dashboard/templates/builds/header.tsx b/src/features/dashboard/templates/builds/header.tsx index d6fd344cb..a0d74c57e 100644 --- a/src/features/dashboard/templates/builds/header.tsx +++ b/src/features/dashboard/templates/builds/header.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import type { BuildStatus } from '@/core/domains/builds/models' +import type { BuildStatus } from '@/core/modules/builds/models' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' import { diff --git a/src/features/dashboard/templates/builds/table-cells.tsx b/src/features/dashboard/templates/builds/table-cells.tsx index 0a86d1302..6a8067f43 100644 --- a/src/features/dashboard/templates/builds/table-cells.tsx +++ b/src/features/dashboard/templates/builds/table-cells.tsx @@ -7,7 +7,7 @@ import { PROTECTED_URLS } from '@/configs/urls' import type { BuildStatus, ListedBuildModel, -} from '@/core/domains/builds/models' +} from '@/core/modules/builds/models' import { useTemplateTableStore } from '@/features/dashboard/templates/list/stores/table-store' import { useRouteParams } from '@/lib/hooks/use-route-params' import { cn } from '@/lib/utils' @@ -176,7 +176,7 @@ export function Status({ status }: StatusProps) { }, } - const { label, icon, variant } = config[status] + const { label, icon, variant } = config[status]! return (
diff --git a/src/features/dashboard/templates/builds/table.tsx b/src/features/dashboard/templates/builds/table.tsx index 0a6034285..3669831ba 100644 --- a/src/features/dashboard/templates/builds/table.tsx +++ b/src/features/dashboard/templates/builds/table.tsx @@ -12,7 +12,7 @@ import { PROTECTED_URLS } from '@/configs/urls' import type { ListedBuildModel, RunningBuildStatusModel, -} from '@/core/domains/builds/models' +} from '@/core/modules/builds/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { cn } from '@/lib/utils/ui' import { useTRPC } from '@/trpc/client' diff --git a/src/features/dashboard/templates/builds/use-filters.tsx b/src/features/dashboard/templates/builds/use-filters.tsx index 4d0d3fd1e..1aad6b61e 100644 --- a/src/features/dashboard/templates/builds/use-filters.tsx +++ b/src/features/dashboard/templates/builds/use-filters.tsx @@ -3,7 +3,7 @@ import { useQueryStates } from 'nuqs' import { useMemo } from 'react' import { useDebounceCallback } from 'usehooks-ts' -import type { BuildStatus } from '@/core/domains/builds/models' +import type { BuildStatus } from '@/core/modules/builds/models' import { INITIAL_BUILD_STATUSES } from './constants' import { templateBuildsFilterParams } from './filter-params' diff --git a/src/features/dashboard/usage/sampling-utils.ts b/src/features/dashboard/usage/sampling-utils.ts index 6084ac04f..094305fc1 100644 --- a/src/features/dashboard/usage/sampling-utils.ts +++ b/src/features/dashboard/usage/sampling-utils.ts @@ -1,5 +1,5 @@ import { startOfISOWeek } from 'date-fns' -import type { UsageResponse } from '@/core/domains/billing/models' +import type { UsageResponse } from '@/core/modules/billing/models' import { HOURLY_SAMPLING_THRESHOLD_DAYS, WEEKLY_SAMPLING_THRESHOLD_DAYS, diff --git a/src/features/dashboard/usage/usage-charts-context.tsx b/src/features/dashboard/usage/usage-charts-context.tsx index 11d835be2..71d8614d6 100644 --- a/src/features/dashboard/usage/usage-charts-context.tsx +++ b/src/features/dashboard/usage/usage-charts-context.tsx @@ -9,7 +9,7 @@ import { useMemo, useState, } from 'react' -import type { UsageResponse } from '@/core/domains/billing/models' +import type { UsageResponse } from '@/core/modules/billing/models' import { fillTimeSeriesWithEmptyPoints } from '@/lib/utils/time-series' import { INITIAL_TIMEFRAME_FALLBACK_RANGE_MS } from './constants' import { diff --git a/src/features/general-analytics-collector.tsx b/src/features/general-analytics-collector.tsx index 5ff52b619..057350c7a 100644 --- a/src/features/general-analytics-collector.tsx +++ b/src/features/general-analytics-collector.tsx @@ -2,7 +2,7 @@ import { usePostHog } from 'posthog-js/react' import { useEffect } from 'react' -import { supabase } from '@/lib/clients/supabase/client' +import { supabase } from '@/core/shared/clients/supabase/client' export function GeneralAnalyticsCollector() { const posthog = usePostHog() diff --git a/src/lib/captcha/turnstile.ts b/src/lib/captcha/turnstile.ts index 13aae6a20..82fc7c952 100644 --- a/src/lib/captcha/turnstile.ts +++ b/src/lib/captcha/turnstile.ts @@ -1,7 +1,7 @@ 'use server' import { CAPTCHA_REQUIRED_SERVER } from '@/configs/flags' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' interface TurnstileResponse { success: boolean diff --git a/src/lib/hooks/use-team.ts b/src/lib/hooks/use-team.ts index d7074e63b..80bd863e8 100644 --- a/src/lib/hooks/use-team.ts +++ b/src/lib/hooks/use-team.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useDebounceCallback } from 'usehooks-ts' -import type { ClientTeam } from '@/core/domains/teams/models' +import type { ClientTeam } from '@/core/modules/teams/models' import { useDashboard } from '@/features/dashboard/context' export const useTeamCookieManager = () => { diff --git a/src/types/api.types.ts b/src/types/api.types.ts index a01590613..eaec2186a 100644 --- a/src/types/api.types.ts +++ b/src/types/api.types.ts @@ -1,4 +1,4 @@ -import type { components as InfraComponents } from '@/types/infra-api.types' +import type { components as InfraComponents } from '@/contracts/infra-api' type Sandbox = InfraComponents['schemas']['ListedSandbox'] diff --git a/src/ui/error.tsx b/src/ui/error.tsx index f51c67434..e01b493a9 100644 --- a/src/ui/error.tsx +++ b/src/ui/error.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary' import { serializeError } from 'serialize-error' -import { l } from '@/lib/clients/logger/logger' +import { l } from '@/core/shared/clients/logger/logger' import { cn } from '@/lib/utils' import { ErrorIndicator } from './error-indicator' import Frame from './frame' diff --git a/tsconfig.json b/tsconfig.json index ee11f05e8..ffdaf4893 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,50 +20,15 @@ } ], "paths": { - "@/domains/*": ["./src/core/domains/*"], - "@/shared/*": ["./src/core/shared/*"], - "@/lib/clients/action": ["./src/core/server/actions/client.ts"], - "@/lib/clients/*": ["./src/core/shared/clients/*"], - "@/lib/utils/action": ["./src/core/server/actions/utils.ts"], - "@/lib/schemas/*": ["./src/core/shared/schemas/*"], - "@/types/argus-api.types": [ + "@/contracts/argus-api": [ "./src/core/shared/contracts/argus-api.types.ts" ], - "@/types/dashboard-api.types": [ + "@/contracts/dashboard-api": [ "./src/core/shared/contracts/dashboard-api.types.ts" ], - "@/types/infra-api.types": [ + "@/contracts/infra-api": [ "./src/core/shared/contracts/infra-api.types.ts" ], - "@/types/database.types": [ - "./src/core/shared/contracts/database.types.ts" - ], - "@/types/errors": ["./src/core/shared/errors.ts"], - "@/server/api/errors": ["./src/core/server/adapters/trpc-errors.ts"], - "@/server/api/models/builds.models": [ - "./src/core/domains/builds/models.ts" - ], - "@/server/api/models/sandboxes.models": [ - "./src/core/domains/sandboxes/models.ts" - ], - "@/server/api/models/auth.models": ["./src/core/domains/auth/models.ts"], - "@/server/api/schemas/sandboxes": [ - "./src/core/domains/sandboxes/schemas.ts" - ], - "@/server/api/init": ["./src/core/server/trpc/init.ts"], - "@/server/api/procedures": ["./src/core/server/trpc/procedures.ts"], - "@/server/api/routers": ["./src/core/server/api/routers/index.ts"], - "@/server/api/routers/*": ["./src/core/server/api/routers/*"], - "@/server/api/middlewares/*": ["./src/core/server/api/middlewares/*"], - "@/server/*": [ - "./src/core/server/actions/*", - "./src/core/server/context/*", - "./src/core/server/adapters/*", - "./src/core/server/trpc/*", - "./src/core/server/http/*", - "./src/core/server/functions/*", - "./src/core/server/api/*" - ], "@/*": ["./src/*"] }, "isolatedModules": true From e127692dc4ef618ccc8ee1b6dda819862db5291d Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 19 Mar 2026 20:57:07 -0700 Subject: [PATCH 11/37] chore: format --- .../dashboard/settings/general/profile-picture-card.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index 879374ed8..c8d52113e 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -1,7 +1,7 @@ 'use client' -import { AnimatePresence, motion } from 'framer-motion' import { useQueryClient } from '@tanstack/react-query' +import { AnimatePresence, motion } from 'framer-motion' import { ChevronsUp, ImagePlusIcon, Loader2, Pencil } from 'lucide-react' import { useAction } from 'next-safe-action/hooks' import { useRef, useState } from 'react' @@ -63,7 +63,6 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { const handleUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { - const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB in bytes if (file.size > MAX_FILE_SIZE) { From 46c1c689e6155367f8f7e9c4d3ab2f59e9f8cf08 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 20 Mar 2026 11:11:26 -0700 Subject: [PATCH 12/37] refactor: update team model and related imports - Renamed `ClientTeam` to `TeamModel` across the codebase for consistency with the new API structure. - Updated references to team properties, changing `is_default` to `isDefault`. - Removed the obsolete `get-team-limits` function and its related types. - Adjusted various components and repositories to align with the new team model and limits structure. --- spec/openapi.dashboard-api.yaml | 10 +++ src/__test__/unit/team-display-name.test.ts | 42 +++++++++++++ src/app/(auth)/auth/cli/page.tsx | 2 +- src/app/api/teams/user/types.ts | 4 +- .../[teamIdOrSlug]/billing/plan/page.tsx | 1 - src/app/dashboard/unauthorized.tsx | 6 +- src/app/sbx/new/route.ts | 3 +- src/core/modules/teams/models.ts | 16 +---- .../modules/teams/teams-repository.server.ts | 44 +------------ .../teams/user-teams-repository.server.ts | 46 ++------------ src/core/modules/teams/utils.ts | 22 +++++++ src/core/server/api/routers/billing.ts | 9 --- .../server/functions/team/get-team-limits.ts | 62 ------------------- .../functions/team/resolve-user-team.ts | 2 +- .../shared/contracts/dashboard-api.types.ts | 3 + src/features/dashboard/billing/addons.tsx | 5 +- .../concurrent-sandboxes-addon-dialog.tsx | 10 +-- src/features/dashboard/billing/hooks.ts | 15 ----- .../dashboard/billing/select-plan.tsx | 3 +- .../dashboard/billing/selected-plan.tsx | 13 ++-- src/features/dashboard/billing/types.ts | 2 +- src/features/dashboard/context.tsx | 10 +-- .../sandbox/inspect/incompatible.tsx | 6 +- .../sandboxes/monitoring/charts/charts.tsx | 20 ++---- .../charts/concurrent-chart/index.tsx | 12 ++-- .../sandboxes/monitoring/header.client.tsx | 27 +++++++- .../dashboard/sandboxes/monitoring/header.tsx | 55 +++++----------- .../dashboard/settings/general/name-card.tsx | 8 ++- .../settings/general/profile-picture-card.tsx | 10 +-- .../dashboard/sidebar/blocked-banner.tsx | 19 +++--- src/features/dashboard/sidebar/menu-teams.tsx | 9 +-- src/features/dashboard/sidebar/menu.tsx | 9 +-- src/lib/hooks/use-team.ts | 4 +- 33 files changed, 205 insertions(+), 304 deletions(-) create mode 100644 src/__test__/unit/team-display-name.test.ts create mode 100644 src/core/modules/teams/utils.ts delete mode 100644 src/core/server/functions/team/get-team-limits.ts diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml index 263b643dd..9811ebcca 100644 --- a/spec/openapi.dashboard-api.yaml +++ b/spec/openapi.dashboard-api.yaml @@ -381,6 +381,9 @@ components: - tier - email - profilePictureUrl + - isBlocked + - isBanned + - blockedReason - isDefault - limits properties: @@ -398,6 +401,13 @@ components: profilePictureUrl: type: string nullable: true + isBlocked: + type: boolean + isBanned: + type: boolean + blockedReason: + type: string + nullable: true isDefault: type: boolean limits: diff --git a/src/__test__/unit/team-display-name.test.ts b/src/__test__/unit/team-display-name.test.ts new file mode 100644 index 000000000..377808f31 --- /dev/null +++ b/src/__test__/unit/team-display-name.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { + getTeamDisplayName, + getTransformedDefaultTeamName, +} from '@/core/modules/teams/utils' + +describe('team display name', () => { + it('transforms unchanged default team email names', () => { + const transformed = getTransformedDefaultTeamName({ + email: 'ben.fornefeld@gmail.com', + isDefault: true, + name: 'ben.fornefeld@gmail.com', + }) + + expect(transformed).toBe("Ben.fornefeld's Team") + expect( + getTeamDisplayName({ + email: 'ben.fornefeld@gmail.com', + isDefault: true, + name: 'ben.fornefeld@gmail.com', + }) + ).toBe("Ben.fornefeld's Team") + }) + + it('falls back to the raw team name otherwise', () => { + expect( + getTransformedDefaultTeamName({ + email: 'ben.fornefeld@gmail.com', + isDefault: true, + name: 'Platform Team', + }) + ).toBeNull() + + expect( + getTeamDisplayName({ + email: 'ben.fornefeld@gmail.com', + isDefault: false, + name: 'Platform Team', + }) + ).toBe('Platform Team') + }) +}) diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index ba12c67b0..a336cac8b 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -37,7 +37,7 @@ async function handleCLIAuth( } const defaultTeam = - teamsResult.data.find((team) => team.is_default) ?? teamsResult.data[0] + teamsResult.data.find((team) => team.isDefault) ?? teamsResult.data[0] if (!defaultTeam) { throw new Error('Failed to resolve default team') diff --git a/src/app/api/teams/user/types.ts b/src/app/api/teams/user/types.ts index c4c0809d8..db873be51 100644 --- a/src/app/api/teams/user/types.ts +++ b/src/app/api/teams/user/types.ts @@ -1,3 +1,3 @@ -import type { ClientTeam } from '@/core/modules/teams/models' +import type { TeamModel } from '@/core/modules/teams/models' -export type UserTeamsResponse = { teams: ClientTeam[] } +export type UserTeamsResponse = { teams: TeamModel[] } diff --git a/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx b/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx index 35d6b76b0..2f66aa4c3 100644 --- a/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx @@ -10,7 +10,6 @@ export default async function BillingPlanPage({ const { teamIdOrSlug } = await params prefetch(trpc.billing.getItems.queryOptions({ teamIdOrSlug })) - prefetch(trpc.billing.getTeamLimits.queryOptions({ teamIdOrSlug })) return ( diff --git a/src/app/dashboard/unauthorized.tsx b/src/app/dashboard/unauthorized.tsx index 2d38b35b9..6a6a8fb8f 100644 --- a/src/app/dashboard/unauthorized.tsx +++ b/src/app/dashboard/unauthorized.tsx @@ -59,9 +59,9 @@ export default function Unauthorized() { {/* Background pattern */}
-
- - +
+ +
diff --git a/src/app/sbx/new/route.ts b/src/app/sbx/new/route.ts index 96e59b8ef..516a43aa5 100644 --- a/src/app/sbx/new/route.ts +++ b/src/app/sbx/new/route.ts @@ -39,8 +39,7 @@ export const GET = async (req: NextRequest) => { accessToken: session.access_token, }).listUserTeams() const defaultTeam = teamsResult.ok - ? (teamsResult.data.find((team) => team.is_default) ?? - teamsResult.data[0]) + ? (teamsResult.data.find((team) => team.isDefault) ?? teamsResult.data[0]) : null if (!defaultTeam) { diff --git a/src/core/modules/teams/models.ts b/src/core/modules/teams/models.ts index f53883629..b77242521 100644 --- a/src/core/modules/teams/models.ts +++ b/src/core/modules/teams/models.ts @@ -1,17 +1,7 @@ -import type { Database } from '@/core/shared/contracts/database.types' +import type { components as DashboardComponents } from '@/contracts/dashboard-api' -export type ClientTeam = Database['public']['Tables']['teams']['Row'] & { - is_default?: boolean - transformed_default_name?: string -} - -export type TeamLimits = { - concurrentInstances: number - diskMb: number - maxLengthHours: number - maxRamMb: number - maxVcpu: number -} +export type TeamModel = DashboardComponents['schemas']['UserTeam'] +export type TeamLimits = DashboardComponents['schemas']['UserTeamLimits'] export type TeamMemberInfo = { id: string diff --git a/src/core/modules/teams/teams-repository.server.ts b/src/core/modules/teams/teams-repository.server.ts index 605c86343..0ad43c54e 100644 --- a/src/core/modules/teams/teams-repository.server.ts +++ b/src/core/modules/teams/teams-repository.server.ts @@ -8,18 +8,7 @@ import type { components as DashboardComponents } from '@/core/shared/contracts/ import { repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import type { TeamLimits, TeamMember } from './models' - -type ApiUserTeam = { - id: string - limits: { - concurrentSandboxes: number - diskMb: number - maxLengthHours: number - maxRamMb: number - maxVcpu: number - } -} +import type { TeamMember } from './models' type TeamsRepositoryDeps = { apiClient: typeof api @@ -30,7 +19,6 @@ type TeamsRepositoryDeps = { export type TeamsRequestScope = TeamRequestScope export interface TeamsRepository { - getTeamLimits(): Promise> listTeamMembers(): Promise> updateTeamName( name: string @@ -66,36 +54,6 @@ export function createTeamsRepository( } ): TeamsRepository { return { - async getTeamLimits(): Promise> { - const { data, error, response } = await deps.apiClient.GET('/teams', { - headers: deps.authHeaders(scope.accessToken, scope.teamId), - }) - - if (!response.ok || error || !data?.teams) { - return err( - repoErrorFromHttp( - response.status, - error?.message ?? 'Failed to fetch team limits', - error - ) - ) - } - - const teams = data.teams as ApiUserTeam[] - const team = teams.find((candidate) => candidate.id === scope.teamId) - - if (!team) { - return err(repoErrorFromHttp(404, 'Team not found')) - } - - return ok({ - concurrentInstances: team.limits.concurrentSandboxes, - diskMb: team.limits.diskMb, - maxLengthHours: team.limits.maxLengthHours, - maxRamMb: team.limits.maxRamMb, - maxVcpu: team.limits.maxVcpu, - }) - }, async listTeamMembers(): Promise> { const { data, error, response } = await deps.apiClient.GET( '/teams/{teamId}/members', diff --git a/src/core/modules/teams/user-teams-repository.server.ts b/src/core/modules/teams/user-teams-repository.server.ts index 19867bce9..68f200089 100644 --- a/src/core/modules/teams/user-teams-repository.server.ts +++ b/src/core/modules/teams/user-teams-repository.server.ts @@ -6,41 +6,7 @@ import { api } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { RequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' -import type { ClientTeam, ResolvedTeam } from './models' - -type ApiUserTeam = { - id: string - name: string - slug: string - tier: string - email: string - profilePictureUrl: string | null - isDefault: boolean - limits: { - concurrentSandboxes: number - diskMb: number - maxLengthHours: number - maxRamMb: number - maxVcpu: number - } -} - -function mapApiTeamToClientTeam(apiTeam: ApiUserTeam): ClientTeam { - return { - id: apiTeam.id, - name: apiTeam.name, - slug: apiTeam.slug, - tier: apiTeam.tier, - email: apiTeam.email, - is_default: apiTeam.isDefault, - is_banned: false, - is_blocked: false, - blocked_reason: null, - cluster_id: null, - created_at: '', - profile_picture_url: apiTeam.profilePictureUrl, - } -} +import type { ResolvedTeam, TeamModel } from './models' type UserTeamsRepositoryDeps = { apiClient: typeof api @@ -50,7 +16,7 @@ type UserTeamsRepositoryDeps = { export type UserTeamsRequestScope = RequestScope export interface UserTeamsRepository { - listUserTeams(): Promise> + listUserTeams(): Promise> resolveTeamBySlug( slug: string, next?: { tags?: string[] } @@ -64,7 +30,7 @@ export function createUserTeamsRepository( authHeaders: SUPABASE_AUTH_HEADERS, } ): UserTeamsRepository { - const listApiUserTeams = async (): Promise> => { + const listApiUserTeams = async (): Promise> => { const { data, error, response } = await deps.apiClient.GET('/teams', { headers: deps.authHeaders(scope.accessToken), }) @@ -79,18 +45,18 @@ export function createUserTeamsRepository( ) } - return ok(data.teams as ApiUserTeam[]) + return ok(data.teams) } return { - async listUserTeams(): Promise> { + async listUserTeams(): Promise> { const teamsResult = await listApiUserTeams() if (!teamsResult.ok) { return teamsResult } - return ok(teamsResult.data.map(mapApiTeamToClientTeam)) + return ok(teamsResult.data) }, async resolveTeamBySlug( slug: string, diff --git a/src/core/modules/teams/utils.ts b/src/core/modules/teams/utils.ts new file mode 100644 index 000000000..702eefc9b --- /dev/null +++ b/src/core/modules/teams/utils.ts @@ -0,0 +1,22 @@ +import type { TeamModel } from './models' + +export function getTransformedDefaultTeamName( + team: Pick +): string | null { + if (!team.isDefault || team.name !== team.email) { + return null + } + + const [username] = team.email.split('@') + if (!username) { + return null + } + + return `${username.charAt(0).toUpperCase()}${username.slice(1)}'s Team` +} + +export function getTeamDisplayName( + team: Pick +): string { + return getTransformedDefaultTeamName(team) ?? team.name +} diff --git a/src/core/server/api/routers/billing.ts b/src/core/server/api/routers/billing.ts index 261437e92..1e48fe5dd 100644 --- a/src/core/server/api/routers/billing.ts +++ b/src/core/server/api/routers/billing.ts @@ -74,15 +74,6 @@ export const billingRouter = createTRPCRouter({ return result.data }), - getTeamLimits: billingAndTeamsRepositoryProcedure.query(async ({ ctx }) => { - const limitsResult = await ctx.teamsRepository.getTeamLimits() - if (!limitsResult.ok) { - throwTRPCErrorFromRepoError(limitsResult.error) - } - - return limitsResult.data - }), - setLimit: billingRepositoryProcedure .input( z.object({ diff --git a/src/core/server/functions/team/get-team-limits.ts b/src/core/server/functions/team/get-team-limits.ts deleted file mode 100644 index fdf4c9dcd..000000000 --- a/src/core/server/functions/team/get-team-limits.ts +++ /dev/null @@ -1,62 +0,0 @@ -import 'server-only' - -import { cache } from 'react' -import { z } from 'zod' -import { USE_MOCK_DATA } from '@/configs/flags' -import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server' -import { - authActionClient, - withTeamIdResolution, -} from '@/core/server/actions/client' -import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' - -export interface TeamLimits { - concurrentInstances: number - diskMb: number - maxLengthHours: number - maxRamMb: number - maxVcpu: number -} - -const MOCK_TIER_LIMITS: TeamLimits = { - concurrentInstances: 100_000, - diskMb: 102400, - maxLengthHours: 24, - maxRamMb: 65536, - maxVcpu: 32, -} - -const GetTeamLimitsSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, -}) - -const getTeamLimitsCached = cache( - async (accessToken: string, teamId: string) => { - return createTeamsRepository({ - accessToken, - teamId, - }).getTeamLimits() - } -) - -export const getTeamLimits = authActionClient - .schema(GetTeamLimitsSchema) - .metadata({ serverFunctionName: 'getTeamLimits' }) - .use(withTeamIdResolution) - .action(async ({ ctx }) => { - if (USE_MOCK_DATA) { - return MOCK_TIER_LIMITS - } - - const limitsResult = await getTeamLimitsCached( - ctx.session.access_token, - ctx.teamId - ) - - if (!limitsResult.ok) { - return toActionErrorFromRepoError(limitsResult.error) - } - - return limitsResult.data - }) diff --git a/src/core/server/functions/team/resolve-user-team.ts b/src/core/server/functions/team/resolve-user-team.ts index 5da43f5b8..5f061fd4a 100644 --- a/src/core/server/functions/team/resolve-user-team.ts +++ b/src/core/server/functions/team/resolve-user-team.ts @@ -36,7 +36,7 @@ export async function resolveUserTeam( return null } - const defaultTeam = teamsResult.data.find((t) => t.is_default) + const defaultTeam = teamsResult.data.find((t) => t.isDefault) const team = defaultTeam ?? teamsResult.data[0] if (!team) { diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts index 6d8346fa5..78e5bf323 100644 --- a/src/core/shared/contracts/dashboard-api.types.ts +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -655,6 +655,9 @@ export interface components { tier: string email: string profilePictureUrl: string | null + isBlocked: boolean + isBanned: boolean + blockedReason: string | null isDefault: boolean limits: components['schemas']['UserTeamLimits'] } diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index 2d7266b41..8557780a1 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -19,7 +19,7 @@ import { Skeleton } from '@/ui/primitives/skeleton' import { useDashboard } from '../context' import { ConcurrentSandboxAddOnPurchaseDialog } from './concurrent-sandboxes-addon-dialog' import { ADDON_500_SANDBOXES_ID, TIER_PRO_ID } from './constants' -import { useBillingItems, useTeamLimits } from './hooks' +import { useBillingItems } from './hooks' import { formatAddonQuantity } from './utils' interface AddonItemProps { @@ -193,8 +193,7 @@ export default function Addons() { const trpc = useTRPC() const [isDialogOpen, setIsDialogOpen] = useState(false) const { tierData, addonData, isLoading } = useBillingItems() - const { teamLimits } = useTeamLimits() - const currentConcurrentSandboxesLimit = teamLimits?.concurrentInstances ?? 0 + const currentConcurrentSandboxesLimit = team.limits.concurrentSandboxes const selectedTierId = tierData?.selected?.id const currentAddon = addonData?.current diff --git a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx index 17a41935a..1afbeb639 100644 --- a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx +++ b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx @@ -15,6 +15,7 @@ import { } from 'lucide-react' import { useRouter } from 'next/navigation' import { useState } from 'react' +import { DASHBOARD_TEAMS_LIST_QUERY_OPTIONS } from '@/core/application/teams/queries' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { useTRPC } from '@/trpc/client' @@ -95,14 +96,15 @@ function DialogContent_Inner({ const itemsQueryKey = trpc.billing.getItems.queryOptions({ teamIdOrSlug, }).queryKey - const teamLimitsQueryKey = trpc.billing.getTeamLimits.queryOptions({ - teamIdOrSlug, - }).queryKey + const teamListQueryKey = trpc.teams.list.queryOptions( + undefined, + DASHBOARD_TEAMS_LIST_QUERY_OPTIONS + ).queryKey const { confirmPayment, isConfirming } = usePaymentConfirmation({ onSuccess: () => { queryClient.invalidateQueries({ queryKey: itemsQueryKey }) - queryClient.invalidateQueries({ queryKey: teamLimitsQueryKey }) + queryClient.invalidateQueries({ queryKey: teamListQueryKey }) onOpenChange(false) }, onFallbackToPaymentElement: handleSwitchToPaymentElement, diff --git a/src/features/dashboard/billing/hooks.ts b/src/features/dashboard/billing/hooks.ts index a6a413327..9db1fb45b 100644 --- a/src/features/dashboard/billing/hooks.ts +++ b/src/features/dashboard/billing/hooks.ts @@ -318,18 +318,3 @@ export function useInvoices() { error, } } - -export function useTeamLimits() { - const { teamIdOrSlug } = useRouteParams<'/dashboard/[teamIdOrSlug]/billing'>() - const trpc = useTRPC() - - const { data: teamLimits, isLoading } = useQuery({ - ...trpc.billing.getTeamLimits.queryOptions({ teamIdOrSlug }), - throwOnError: true, - }) - - return { - teamLimits, - isLoading, - } -} diff --git a/src/features/dashboard/billing/select-plan.tsx b/src/features/dashboard/billing/select-plan.tsx index 24d1bf180..1bb1b1e6f 100644 --- a/src/features/dashboard/billing/select-plan.tsx +++ b/src/features/dashboard/billing/select-plan.tsx @@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query' import { useRouter } from 'next/navigation' import type { TierInfo } from '@/core/modules/billing/models' +import { getTeamDisplayName } from '@/core/modules/teams/utils' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' @@ -175,7 +176,7 @@ function PlanCard({ ? formatCurrency(tier.price_cents / 100) : 'FREE' - const teamDisplayName = team.transformed_default_name || team.name + const teamDisplayName = getTeamDisplayName(team) const buttonVariant = isBaseTier ? 'outline' : 'default' const buttonText = isBaseTier ? 'Downgrade' : 'Upgrade' diff --git a/src/features/dashboard/billing/selected-plan.tsx b/src/features/dashboard/billing/selected-plan.tsx index aa767facc..52bfa8b9d 100644 --- a/src/features/dashboard/billing/selected-plan.tsx +++ b/src/features/dashboard/billing/selected-plan.tsx @@ -4,7 +4,7 @@ import { useMutation } from '@tanstack/react-query' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { PROTECTED_URLS } from '@/configs/urls' -import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' +import type { TeamLimits } from '@/core/modules/teams/models' import { useRouteParams } from '@/lib/hooks/use-route-params' import { defaultErrorToast, useToast } from '@/lib/hooks/use-toast' import { formatCurrency } from '@/lib/utils/formatting' @@ -23,7 +23,8 @@ import { import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' import { Skeleton } from '@/ui/primitives/skeleton' -import { useBillingItems, useTeamLimits } from './hooks' +import { useDashboard } from '../context' +import { useBillingItems } from './hooks' import { TierAvatarBorder } from './tier-avatar-border' import type { BillingTierData } from './types' import { formatHours, formatMibToGb, formatTierDisplayName } from './utils' @@ -34,15 +35,15 @@ function formatCpu(vcpu: number): string { export default function SelectedPlan() { const { tierData, isLoading: isBillingLoading } = useBillingItems() - const { teamLimits, isLoading: isLimitsLoading } = useTeamLimits() - const isLoading = isBillingLoading || isLimitsLoading + const { team } = useDashboard() + const isLoading = isBillingLoading return (
@@ -213,7 +214,7 @@ function PlanFeatures({ teamLimits, isLoading }: PlanFeaturesProps) { const features = [ { icon: , - label: `${teamLimits.concurrentInstances} concurrent sandboxes`, + label: `${teamLimits.concurrentSandboxes} concurrent sandboxes`, }, { icon: , diff --git a/src/features/dashboard/billing/types.ts b/src/features/dashboard/billing/types.ts index d4383cf7f..45fd3d40e 100644 --- a/src/features/dashboard/billing/types.ts +++ b/src/features/dashboard/billing/types.ts @@ -1,5 +1,5 @@ import type { TeamItems } from '@/core/modules/billing/models' -import type { TeamLimits } from '@/core/server/functions/team/get-team-limits' +import type { TeamLimits } from '@/core/modules/teams/models' export interface BillingData { items: TeamItems diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index e341710b9..dc17044bb 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -2,11 +2,11 @@ import type { User } from '@supabase/supabase-js' import { createContext, type ReactNode, useContext } from 'react' -import type { ClientTeam } from '@/core/modules/teams/models' +import type { TeamModel } from '@/core/modules/teams/models' interface DashboardContextValue { - team: ClientTeam - teams: ClientTeam[] + team: TeamModel + teams: TeamModel[] user: User } @@ -16,8 +16,8 @@ const DashboardContext = createContext( interface DashboardContextProviderProps { children: ReactNode - initialTeam: ClientTeam - initialTeams: ClientTeam[] + initialTeam: TeamModel + initialTeams: TeamModel[] initialUser: User } diff --git a/src/features/dashboard/sandbox/inspect/incompatible.tsx b/src/features/dashboard/sandbox/inspect/incompatible.tsx index 18a2639b2..a17bb39f9 100644 --- a/src/features/dashboard/sandbox/inspect/incompatible.tsx +++ b/src/features/dashboard/sandbox/inspect/incompatible.tsx @@ -42,9 +42,9 @@ export default function SandboxInspectIncompatible({ return (
-
- - +
+ +
- + ) diff --git a/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/index.tsx b/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/index.tsx index e59b70be7..04f7ee7d9 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/index.tsx +++ b/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/index.tsx @@ -1,6 +1,7 @@ 'use client' import { useCallback, useRef } from 'react' +import { useDashboard } from '@/features/dashboard/context' import { ReactiveLiveBadge } from '@/ui/live' import { useTeamMetricsCharts } from '../../charts-context' import { AnimatedMetricDisplay } from '../animated-metric-display' @@ -13,13 +14,8 @@ import { useTimeRangeDisplay, } from './hooks' -interface ConcurrentChartProps { - concurrentInstancesLimit?: number -} - -export default function ConcurrentChartClient({ - concurrentInstancesLimit, -}: ConcurrentChartProps) { +export default function ConcurrentChartClient() { + const { team } = useDashboard() const { data, isPolling, @@ -100,7 +96,7 @@ export default function ConcurrentChartClient({ metrics={data.metrics} step={data.step} timeframe={timeframe} - concurrentLimit={concurrentInstancesLimit} + concurrentLimit={team.limits.concurrentSandboxes} onZoomEnd={(from, end) => setCustomRange(from, end)} onTooltipValueChange={handleTooltipValueChange} onHoverEnd={handleHoverEnd} diff --git a/src/features/dashboard/sandboxes/monitoring/header.client.tsx b/src/features/dashboard/sandboxes/monitoring/header.client.tsx index 72a178897..b66c82ded 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.client.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.client.tsx @@ -4,6 +4,7 @@ import type { InferSafeActionFnResult } from 'next-safe-action' import { useMemo } from 'react' import type { NonUndefined } from 'react-hook-form' import type { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' +import { useDashboard } from '@/features/dashboard/context' import { formatDecimal, formatNumber } from '@/lib/utils/formatting' import { AnimatedNumber } from '@/ui/primitives/animated-number' import { useRecentMetrics } from './hooks/use-recent-metrics' @@ -12,14 +13,14 @@ interface TeamMonitoringHeaderClientProps { initialData: NonUndefined< InferSafeActionFnResult['data'] > - limit?: number } export function ConcurrentSandboxesClient({ initialData, - limit, }: TeamMonitoringHeaderClientProps) { + const { team } = useDashboard() const { data } = useRecentMetrics({ initialData }) + const limit = team.limits.concurrentSandboxes const lastConcurrentSandboxes = formatNumber( data?.metrics?.[(data?.metrics?.length ?? 0) - 1]?.concurrentSandboxes ?? 0 @@ -58,3 +59,25 @@ export function SandboxesStartRateClient({ /> ) } + +interface MaxConcurrentSandboxesClientProps { + concurrentSandboxes: number +} + +export function MaxConcurrentSandboxesClient({ + concurrentSandboxes, +}: MaxConcurrentSandboxesClientProps) { + const { team } = useDashboard() + const limit = team.limits.concurrentSandboxes + + return ( + <> + + {formatNumber(concurrentSandboxes)} + + + LIMIT: {formatNumber(limit)} + + + ) +} diff --git a/src/features/dashboard/sandboxes/monitoring/header.tsx b/src/features/dashboard/sandboxes/monitoring/header.tsx index 2a23eaf1e..0009ab6c2 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.tsx @@ -2,14 +2,13 @@ import { AlertTriangle } from 'lucide-react' import { Suspense } from 'react' import { getTeamMetrics } from '@/core/server/functions/sandboxes/get-team-metrics' import { getTeamMetricsMax } from '@/core/server/functions/sandboxes/get-team-metrics-max' -import { getTeamLimits } from '@/core/server/functions/team/get-team-limits' -import { formatNumber } from '@/lib/utils/formatting' import { getNowMemo } from '@/lib/utils/server' import ErrorTooltip from '@/ui/error-tooltip' import { SemiLiveBadge } from '@/ui/live' import { Skeleton } from '@/ui/primitives/skeleton' import { ConcurrentSandboxesClient, + MaxConcurrentSandboxesClient, SandboxesStartRateClient, } from './header.client' import { MAX_DAYS_AGO } from './time-picker/constants' @@ -106,14 +105,11 @@ export const ConcurrentSandboxes = async ({ const now = getNowMemo() const start = now - 60_000 - const [teamMetricsResult, teamLimitsResult] = await Promise.all([ - getTeamMetrics({ - teamIdOrSlug, - startDate: start, - endDate: now, - }), - getTeamLimits({ teamIdOrSlug }), - ]) + const teamMetricsResult = await getTeamMetrics({ + teamIdOrSlug, + startDate: start, + endDate: now, + }) if (!teamMetricsResult?.data || teamMetricsResult.serverError) { return ( @@ -124,12 +120,7 @@ export const ConcurrentSandboxes = async ({ ) } - return ( - - ) + return } export const SandboxesStartRate = async ({ @@ -171,15 +162,12 @@ export const MaxConcurrentSandboxes = async ({ const end = Date.now() const start = end - (MAX_DAYS_AGO - 60_000) // 1 minute margin to avoid validation errors - const [teamMetricsResult, teamLimitsResult] = await Promise.all([ - getTeamMetricsMax({ - teamIdOrSlug, - startDate: start, - endDate: end, - metric: 'concurrent_sandboxes', - }), - getTeamLimits({ teamIdOrSlug }), - ]) + const teamMetricsResult = await getTeamMetricsMax({ + teamIdOrSlug, + startDate: start, + endDate: end, + metric: 'concurrent_sandboxes', + }) if (!teamMetricsResult?.data || teamMetricsResult.serverError) { return ( @@ -190,20 +178,9 @@ export const MaxConcurrentSandboxes = async ({ ) } - const limit = teamLimitsResult?.data?.concurrentInstances - - const concurrentSandboxes = teamMetricsResult.data.value - return ( - <> - - {formatNumber(concurrentSandboxes)} - - {!!limit && ( - - LIMIT: {formatNumber(limit)} - - )} - + ) } diff --git a/src/features/dashboard/settings/general/name-card.tsx b/src/features/dashboard/settings/general/name-card.tsx index 8c5fdba3a..d175be456 100644 --- a/src/features/dashboard/settings/general/name-card.tsx +++ b/src/features/dashboard/settings/general/name-card.tsx @@ -5,6 +5,7 @@ import { useHookFormOptimisticAction } from '@next-safe-action/adapter-react-hoo import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'motion/react' import { USER_MESSAGES } from '@/configs/user-messages' +import { getTransformedDefaultTeamName } from '@/core/modules/teams/utils' import { updateTeamNameAction } from '@/core/server/actions/team-actions' import { UpdateTeamNameSchema } from '@/core/server/functions/team/types' import { useDashboard } from '@/features/dashboard/context' @@ -93,6 +94,9 @@ export function NameCard({ className }: NameCardProps) { ) const { watch } = form + const displayedNameHint = getTransformedDefaultTeamName( + optimisticState?.team ?? team + ) return ( @@ -117,7 +121,7 @@ export function NameCard({ className }: NameCardProps) { - {team.transformed_default_name && ( + {displayedNameHint && ( Seen as -{' '} - {team.transformed_default_name} + {displayedNameHint} )} diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index c8d52113e..4e7e286b7 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -95,7 +95,9 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { return ( <> -
setIsHovered(true)} @@ -105,13 +107,13 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { className={cn( 'relative h-24 w-24', { - 'border-none drop-shadow-lg filter': team.profile_picture_url, + 'border-none drop-shadow-lg filter': team.profilePictureUrl, }, className )} > @@ -175,7 +177,7 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { ) : null} -
+ team.blocked_reason?.toLowerCase().includes('billing limit'), - [team.blocked_reason] + () => team.blockedReason?.toLowerCase().includes('billing limit'), + [team.blockedReason] ) + const handleClick = () => { if (isBillingLimit) { router.push(PROTECTED_URLS.LIMITS(team.slug)) @@ -34,19 +35,17 @@ export default function TeamBlockageAlert({ return ( - {team.is_blocked && ( + {team.isBlocked && ( - +
Team is Blocked - {team.blocked_reason && ( + {team.blockedReason && ( - {team.blocked_reason} + {team.blockedReason} )}
diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index 2c0949beb..089ba13d1 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -1,7 +1,8 @@ import Link from 'next/link' import { usePathname, useSearchParams } from 'next/navigation' import { useCallback } from 'react' -import type { ClientTeam } from '@/core/modules/teams/models' +import type { TeamModel } from '@/core/modules/teams/models' +import { getTeamDisplayName } from '@/core/modules/teams/utils' import { useTeamCookieManager } from '@/lib/hooks/use-team' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { @@ -23,7 +24,7 @@ export default function DashboardSidebarMenuTeams() { useTeamCookieManager() const getNextUrl = useCallback( - (team: ClientTeam) => { + (team: TeamModel) => { const splitPath = pathname.split('/') splitPath[2] = team.slug @@ -53,13 +54,13 @@ export default function DashboardSidebarMenuTeams() { - + {team.name?.charAt(0).toUpperCase() || '?'} - {team.transformed_default_name || team.name} + {getTeamDisplayName(team)} diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index ba2204015..8beb64593 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -4,6 +4,7 @@ import { ChevronsUpDown, LogOut, Plus, UserRoundCog } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' +import { getTeamDisplayName } from '@/core/modules/teams/utils' import { signOutAction } from '@/core/server/actions/auth-actions' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' @@ -44,7 +45,7 @@ export default function DashboardSidebarMenu({ size="lg" className={cn( 'h-14 flex', - 'group-data-[collapsible=icon]:h-9 group-data-[collapsible=icon]:border-0 group-data-[collapsible=icon]:!px-0', + 'group-data-[collapsible=icon]:h-9 group-data-[collapsible=icon]:border-0 group-data-[collapsible=icon]:px-0!', className )} > @@ -53,12 +54,12 @@ export default function DashboardSidebarMenu({ 'shrink-0 transition-all duration-100 ease-in-out', 'group-data-[collapsible=icon]:block group-data-[collapsible=icon]:size-9 group-data-[collapsible=icon]:p-[5px]', { - 'drop-shadow-sm filter': team.profile_picture_url, + 'drop-shadow-sm filter': team.profilePictureUrl, } )} > @@ -70,7 +71,7 @@ export default function DashboardSidebarMenu({ TEAM - {team.transformed_default_name || team.name} + {getTeamDisplayName(team)}
diff --git a/src/lib/hooks/use-team.ts b/src/lib/hooks/use-team.ts index 80bd863e8..fb52e2f8e 100644 --- a/src/lib/hooks/use-team.ts +++ b/src/lib/hooks/use-team.ts @@ -2,14 +2,14 @@ import { useEffect } from 'react' import { useDebounceCallback } from 'usehooks-ts' -import type { ClientTeam } from '@/core/modules/teams/models' +import type { TeamModel } from '@/core/modules/teams/models' import { useDashboard } from '@/features/dashboard/context' export const useTeamCookieManager = () => { const { team } = useDashboard() const updateTeamCookieState = useDebounceCallback( - async (iTeam: ClientTeam) => { + async (iTeam: TeamModel) => { await fetch('/api/team/state', { method: 'POST', body: JSON.stringify({ From 5bb9b48d9e2c32dc129a75ee9e9145177e4d134f Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 20 Mar 2026 14:21:54 -0700 Subject: [PATCH 13/37] refactor: deprecate teamIdOrSlug in favor of teamSlug --- .../integration/dashboard-route.test.ts | 25 +- .../integration/resolve-user-team.test.ts | 555 +++--------------- .../{[teamId] => [teamSlug]}/metrics/route.ts | 24 +- .../{[teamId] => [teamSlug]}/metrics/types.ts | 0 .../sandboxes/metrics/route.ts | 20 +- .../sandboxes/metrics/types.ts | 0 .../sandboxes/[sandboxId]/logs/page.tsx | 11 - .../account/page.tsx | 0 .../billing/page.tsx | 10 +- .../billing/plan/page.tsx | 6 +- .../billing/plan/select/page.tsx | 6 +- .../general/page.tsx | 2 +- .../keys/page.tsx | 2 +- .../{[teamIdOrSlug] => [teamSlug]}/layout.tsx | 12 +- .../limits/loading.tsx | 0 .../limits/page.tsx | 6 +- .../members/page.tsx | 2 +- .../not-found.tsx | 0 .../sandboxes/(tabs)/@list/default.tsx | 0 .../sandboxes/(tabs)/@list/page.tsx | 6 +- .../sandboxes/(tabs)/@monitoring/default.tsx | 0 .../sandboxes/(tabs)/@monitoring/page.tsx | 2 +- .../sandboxes/(tabs)/layout.tsx | 2 +- .../sandboxes/(tabs)/page.tsx | 0 .../[sandboxId]/filesystem/loading.tsx | 0 .../sandboxes/[sandboxId]/filesystem/page.tsx | 0 .../sandboxes/[sandboxId]/layout.tsx | 6 +- .../sandboxes/[sandboxId]/logs/loading.tsx | 0 .../sandboxes/[sandboxId]/logs/page.tsx | 11 + .../[sandboxId]/monitoring/loading.tsx | 0 .../sandboxes/[sandboxId]/monitoring/page.tsx | 2 +- .../team-gate.tsx | 9 +- .../templates/(tabs)/@builds/default.tsx | 0 .../templates/(tabs)/@builds/page.tsx | 0 .../templates/(tabs)/@list/default.tsx | 0 .../templates/(tabs)/@list/page.tsx | 0 .../templates/(tabs)/layout.tsx | 2 +- .../[templateId]/builds/[buildId]/loading.tsx | 0 .../[templateId]/builds/[buildId]/page.tsx | 8 +- .../usage/loading.tsx | 0 .../usage/page.tsx | 6 +- .../webhooks/page.tsx | 2 +- src/app/dashboard/account/route.ts | 4 +- src/app/dashboard/route.ts | 28 +- src/configs/cache.ts | 2 +- src/configs/keys.ts | 30 +- src/configs/layout.ts | 18 +- src/configs/mock-data.ts | 2 +- src/configs/sidebar.ts | 22 +- src/configs/urls.ts | 67 ++- src/core/modules/support/repository.server.ts | 2 +- src/core/modules/teams/schemas.ts | 6 +- src/core/server/actions/client.ts | 25 +- src/core/server/actions/key-actions.ts | 20 +- src/core/server/actions/sandbox-actions.ts | 18 +- src/core/server/actions/team-actions.ts | 34 +- src/core/server/actions/webhooks-actions.ts | 38 +- .../server/functions/keys/get-api-keys.ts | 13 +- .../sandboxes/get-team-metrics-max.ts | 8 +- .../functions/sandboxes/get-team-metrics.ts | 8 +- .../team/get-team-id-from-segment.ts | 48 -- .../functions/team/get-team-id-from-slug.ts | 43 ++ .../server/functions/team/get-team-members.ts | 8 +- .../functions/team/resolve-user-team.ts | 18 +- src/core/server/functions/usage/get-usage.ts | 8 +- .../server/functions/webhooks/get-webhooks.ts | 8 +- src/core/server/functions/webhooks/schema.ts | 8 +- src/core/server/trpc/procedures.ts | 14 +- src/core/shared/schemas/team.ts | 2 +- .../account/password-settings-server.tsx | 2 +- src/features/dashboard/billing/addons.tsx | 10 +- .../concurrent-sandboxes-addon-dialog.tsx | 9 +- src/features/dashboard/billing/hooks.ts | 12 +- .../dashboard/billing/select-plan.tsx | 8 +- .../dashboard/billing/selected-plan.tsx | 10 +- .../dashboard/build/build-logs-store.ts | 10 +- src/features/dashboard/build/header-cells.tsx | 5 +- src/features/dashboard/build/logs.tsx | 12 +- .../dashboard/build/use-build-logs.ts | 12 +- src/features/dashboard/layouts/layout.tsx | 2 +- src/features/dashboard/limits/alert-card.tsx | 4 +- src/features/dashboard/limits/limit-card.tsx | 4 +- src/features/dashboard/limits/limit-form.tsx | 10 +- .../dashboard/limits/usage-limits.tsx | 4 +- .../dashboard/members/add-member-form.tsx | 2 +- .../dashboard/members/member-card.tsx | 2 +- .../dashboard/members/member-table-body.tsx | 6 +- .../dashboard/members/member-table-row.tsx | 2 +- .../dashboard/members/member-table.tsx | 2 +- .../dashboard/navbar/report-issue-dialog.tsx | 2 +- src/features/dashboard/sandbox/context.tsx | 6 +- .../dashboard/sandbox/header/kill-button.tsx | 2 +- .../sandbox/inspect/incompatible.tsx | 12 +- .../dashboard/sandbox/inspect/not-found.tsx | 6 +- src/features/dashboard/sandbox/layout.tsx | 6 +- src/features/dashboard/sandbox/logs/logs.tsx | 12 +- .../sandbox/logs/sandbox-logs-store.ts | 10 +- .../sandbox/logs/use-sandbox-logs.ts | 12 +- src/features/dashboard/sandbox/logs/view.tsx | 6 +- .../use-sandbox-monitoring-controller.ts | 10 +- .../list/hooks/use-sandboxes-metrics.tsx | 4 +- .../sandboxes/list/open-sandbox-dialog.tsx | 7 +- .../dashboard/sandboxes/list/table-cells.tsx | 2 +- .../dashboard/sandboxes/list/table-row.tsx | 5 +- .../dashboard/sandboxes/list/table.tsx | 5 +- .../sandboxes/live-counter.server.tsx | 8 +- .../sandboxes/monitoring/charts-context.tsx | 4 +- .../sandboxes/monitoring/charts/charts.tsx | 6 +- .../charts/concurrent-chart/hooks.ts | 2 +- .../charts/startrate-chart/hooks.ts | 2 +- .../dashboard/sandboxes/monitoring/header.tsx | 14 +- .../monitoring/hooks/use-recent-metrics.ts | 10 +- .../dashboard/settings/general/name-card.tsx | 2 +- .../settings/general/profile-picture-card.tsx | 6 +- .../settings/keys/create-api-key-dialog.tsx | 4 +- .../dashboard/settings/keys/table-body.tsx | 6 +- .../dashboard/settings/keys/table-row.tsx | 2 +- .../dashboard/settings/keys/table.tsx | 2 +- .../settings/webhooks/add-edit-dialog.tsx | 8 +- .../settings/webhooks/delete-dialog.tsx | 4 +- .../settings/webhooks/edit-secret-dialog.tsx | 6 +- .../settings/webhooks/table-body.tsx | 6 +- .../dashboard/settings/webhooks/table.tsx | 2 +- src/features/dashboard/sidebar/command.tsx | 2 +- src/features/dashboard/sidebar/content.tsx | 4 +- .../templates/builds/table-cells.tsx | 5 +- .../dashboard/templates/builds/table.tsx | 11 +- .../dashboard/templates/list/table-cells.tsx | 19 +- .../dashboard/templates/list/table.tsx | 5 +- src/lib/utils/server.ts | 16 +- src/proxy.ts | 2 +- 131 files changed, 628 insertions(+), 1004 deletions(-) rename src/app/api/teams/{[teamId] => [teamSlug]}/metrics/route.ts (75%) rename src/app/api/teams/{[teamId] => [teamSlug]}/metrics/types.ts (100%) rename src/app/api/teams/{[teamId] => [teamSlug]}/sandboxes/metrics/route.ts (78%) rename src/app/api/teams/{[teamId] => [teamSlug]}/sandboxes/metrics/types.ts (100%) delete mode 100644 src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/page.tsx rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/account/page.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/billing/page.tsx (63%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/billing/plan/page.tsx (73%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/billing/plan/select/page.tsx (69%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/general/page.tsx (97%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/keys/page.tsx (98%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/layout.tsx (88%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/limits/loading.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/limits/page.tsx (77%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/members/page.tsx (95%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/not-found.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/@list/default.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/@list/page.tsx (81%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/@monitoring/default.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/@monitoring/page.tsx (92%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/layout.tsx (92%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/(tabs)/page.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/filesystem/loading.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/filesystem/page.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/layout.tsx (85%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/logs/loading.tsx (100%) create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/page.tsx rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/monitoring/loading.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/sandboxes/[sandboxId]/monitoring/page.tsx (81%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/team-gate.tsx (85%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/(tabs)/@builds/default.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/(tabs)/@builds/page.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/(tabs)/@list/default.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/(tabs)/@list/page.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/(tabs)/layout.tsx (92%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/[templateId]/builds/[buildId]/loading.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/templates/[templateId]/builds/[buildId]/page.tsx (87%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/usage/loading.tsx (100%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/usage/page.tsx (94%) rename src/app/dashboard/{[teamIdOrSlug] => [teamSlug]}/webhooks/page.tsx (98%) delete mode 100644 src/core/server/functions/team/get-team-id-from-segment.ts create mode 100644 src/core/server/functions/team/get-team-id-from-slug.ts diff --git a/src/__test__/integration/dashboard-route.test.ts b/src/__test__/integration/dashboard-route.test.ts index d3535540c..b36e12154 100644 --- a/src/__test__/integration/dashboard-route.test.ts +++ b/src/__test__/integration/dashboard-route.test.ts @@ -17,7 +17,7 @@ import { PROTECTED_URLS } from '@/configs/urls' * 3. Tab parameter routing - all valid tabs and fallbacks * 4. Error handling - auth errors and missing teams * 5. Cookie setting - team persistence - * 6. URL generation - correct paths for team slug/ID + * 6. URL generation - correct slug-backed paths */ // create hoisted mocks @@ -29,6 +29,7 @@ const { } = vi.hoisted(() => ({ mockSupabaseClient: { auth: { + getSession: vi.fn(), getUser: vi.fn(), signOut: vi.fn(), }, @@ -78,6 +79,9 @@ import { GET, TAB_URL_MAP } from '@/app/dashboard/route' describe('Dashboard Route - Team Resolution Integration Tests', () => { beforeEach(() => { vi.clearAllMocks() + mockSupabaseClient.auth.getSession.mockResolvedValue({ + data: { session: { access_token: 'session-token' } }, + }) }) afterEach(() => { @@ -117,8 +121,8 @@ describe('Dashboard Route - Team Resolution Integration Tests', () => { // execute const response = await GET(request) - // verify: resolveUserTeam was called with user ID - expect(mockResolveUserTeam).toHaveBeenCalledWith('user-123') + // verify: resolveUserTeam was called with session access token + expect(mockResolveUserTeam).toHaveBeenCalledWith('session-token') // verify: redirects to sandboxes page expect(response.status).toBe(307) // temporary redirect @@ -250,20 +254,5 @@ describe('Dashboard Route - Team Resolution Integration Tests', () => { const expectedPath = PROTECTED_URLS.SANDBOXES(testTeamSlug) expect(response.headers.get('location')).toContain(expectedPath) }) - - it('should use team ID as fallback when slug is empty', async () => { - const testTeamId = 'team-id-only' - - mockResolveUserTeam.mockResolvedValue({ - id: testTeamId, - slug: '', - }) - - const request = createRequest({ tab: 'billing' }) - const response = await GET(request) - - const expectedPath = TAB_URL_MAP['billing']!(testTeamId) - expect(response.headers.get('location')).toContain(expectedPath) - }) }) }) diff --git a/src/__test__/integration/resolve-user-team.test.ts b/src/__test__/integration/resolve-user-team.test.ts index 48ff6e01d..10b8bb730 100644 --- a/src/__test__/integration/resolve-user-team.test.ts +++ b/src/__test__/integration/resolve-user-team.test.ts @@ -1,525 +1,150 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { COOKIE_KEYS } from '@/configs/cookies' -/** - * Integration tests for resolveUserTeam function - * - * This function is CRITICAL for security as it resolves which team a user should access. - * It MUST always resolve an authorized team for the user, even if: - * - User has cookies for a team they're not authorized to access - * - User has no cookies at all - * - User has invalid/stale cookies - * - * Test Coverage: - * 1. Valid authorized cookies - should use cookies - * 2. Invalid/unauthorized cookies - should fall back to DB (SECURITY CRITICAL) - * 3. No cookies - should use default team from DB - * 4. No cookies, no default - should use first team from DB - * 5. No teams - should return null - * 6. Partial cookies - should fall back to DB - * 7. Database errors - should handle gracefully - */ - -// create hoisted mocks -const { mockSupabaseAdmin, mockCookieStore, mockCheckUserTeamAuth } = +const { mockCookieStore, mockListUserTeams, mockCreateUserTeamsRepository } = vi.hoisted(() => ({ - mockSupabaseAdmin: { - from: vi.fn(), - }, mockCookieStore: { get: vi.fn(), }, - mockCheckUserTeamAuth: vi.fn(), + mockListUserTeams: vi.fn(), + mockCreateUserTeamsRepository: vi.fn(), })) -vi.mock('@/core/shared/clients/supabase/admin', () => ({ - supabaseAdmin: mockSupabaseAdmin, -})) - vi.mock('next/headers', () => ({ cookies: vi.fn(() => mockCookieStore), })) -vi.mock('@/core/server/functions/auth/check-user-team-auth-cached', () => ({ - checkUserTeamAuth: mockCheckUserTeamAuth, +vi.mock('@/core/modules/teams/user-teams-repository.server', () => ({ + createUserTeamsRepository: mockCreateUserTeamsRepository, })) -// import after mocks are set up import { resolveUserTeam } from '@/core/server/functions/team/resolve-user-team' -describe('resolveUserTeam - Authorization Integration Tests', () => { +function setupCookies(cookieValues: Record) { + mockCookieStore.get.mockImplementation((key: string) => { + const value = cookieValues[key] + return typeof value === 'string' ? { value } : undefined + }) +} + +function createTeam(overrides: Record = {}) { + return { + id: 'team-1', + slug: 'team-one', + isDefault: false, + name: 'Team One', + email: 'team-one@example.com', + ...overrides, + } +} + +describe('resolveUserTeam', () => { beforeEach(() => { vi.clearAllMocks() + mockCreateUserTeamsRepository.mockReturnValue({ + listUserTeams: mockListUserTeams, + }) }) afterEach(() => { vi.resetAllMocks() }) - /** - * Helper to setup cookie mock responses - */ - function setupCookies(cookieValues: Record) { - mockCookieStore.get.mockImplementation((key: string) => { - const value = cookieValues[key] - return value ? { value } : undefined - }) - } - - /** - * Helper to setup database mock responses for users_teams query - */ - function setupDatabaseMock( - teams: Array<{ - team_id: string - is_default: boolean - team: { id: string; slug: string } | null - }> | null, - error: { message: string } | null = null - ) { - const selectMock = vi.fn().mockReturnThis() - const eqMock = vi.fn().mockResolvedValue({ - data: teams, - error, - }) - - mockSupabaseAdmin.from.mockReturnValue({ - select: selectMock, - }) - - selectMock.mockReturnValue({ - eq: eqMock, - }) - - return { selectMock, eqMock } - } - - describe('Valid Authorized Cookies', () => { - it('should use cookies when user is authorized for the team', async () => { - // setup: user has valid cookies - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-456', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'my-team', - }) - - // setup: authorization check passes - mockCheckUserTeamAuth.mockResolvedValue(true) - - // execute - const result = await resolveUserTeam('user-123') - - // verify: authorization was checked - expect(mockCheckUserTeamAuth).toHaveBeenCalledWith('user-123', 'team-456') - - // verify: returns team from cookies - expect(result).toEqual({ - id: 'team-456', - slug: 'my-team', - }) - - // verify: database was NOT queried (cookies used) - expect(mockSupabaseAdmin.from).not.toHaveBeenCalled() - }) - - it('should use cookies for user with multiple teams', async () => { - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-secondary', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'secondary-team', - }) - - mockCheckUserTeamAuth.mockResolvedValue(true) - - const result = await resolveUserTeam('user-123') - - expect(result).toEqual({ - id: 'team-secondary', - slug: 'secondary-team', - }) - expect(mockSupabaseAdmin.from).not.toHaveBeenCalled() - }) - }) - - describe('Invalid/Unauthorized Cookies', () => { - it('should fall back to database when user has cookies for unauthorized team', async () => { - // user has cookies for a team they shouldn't access - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-unauthorized', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'unauthorized-team', - }) - - // setup: authorization check FAILS - mockCheckUserTeamAuth.mockResolvedValue(false) - - // setup: database returns user's actual authorized teams - setupDatabaseMock([ - { - team_id: 'team-authorized', - is_default: true, - team: { id: 'team-authorized', slug: 'authorized-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // verify: authorization was checked - expect(mockCheckUserTeamAuth).toHaveBeenCalledWith( - 'user-123', - 'team-unauthorized' - ) - - // verify: returns authorized team from DB, NOT from cookies - expect(result).toEqual({ - id: 'team-authorized', - slug: 'authorized-team', - }) - - // verify: database was queried (fallback) - expect(mockSupabaseAdmin.from).toHaveBeenCalled() - }) - - it('should use first team when unauthorized cookies and no default team', async () => { - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-unauthorized', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'unauthorized-team', - }) - - mockCheckUserTeamAuth.mockResolvedValue(false) - - // no default team, but has teams - setupDatabaseMock([ - { - team_id: 'team-first', - is_default: false, - team: { id: 'team-first', slug: 'first-team' }, - }, - { - team_id: 'team-second', - is_default: false, - team: { id: 'team-second', slug: 'second-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should use first team from list - expect(result).toEqual({ - id: 'team-first', - slug: 'first-team', - }) + it('returns the cookie-backed team when both cookies exist', async () => { + setupCookies({ + [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-cookie-id', + [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'team-cookie-slug', }) - it('should return null when unauthorized cookies and no teams in DB', async () => { - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-unauthorized', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'unauthorized-team', - }) - - mockCheckUserTeamAuth.mockResolvedValue(false) + const result = await resolveUserTeam('access-token') - // user has no teams - setupDatabaseMock([]) - - const result = await resolveUserTeam('user-123') - - expect(result).toBeNull() + expect(result).toEqual({ + id: 'team-cookie-id', + slug: 'team-cookie-slug', }) + expect(mockCreateUserTeamsRepository).not.toHaveBeenCalled() }) - describe('No Cookies', () => { - it('should use default team from database when no cookies exist', async () => { - // no cookies - setupCookies({}) - - // setup: database returns teams with a default - setupDatabaseMock([ - { - team_id: 'team-default', - is_default: true, - team: { id: 'team-default', slug: 'default-team' }, - }, - { - team_id: 'team-other', - is_default: false, - team: { id: 'team-other', slug: 'other-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // verify: did not check authorization (no cookies to check) - expect(mockCheckUserTeamAuth).not.toHaveBeenCalled() - - // verify: returns default team - expect(result).toEqual({ - id: 'team-default', - slug: 'default-team', - }) - }) - - it('should use first team when no cookies and no default team', async () => { - setupCookies({}) - - // no default team - setupDatabaseMock([ - { - team_id: 'team-first', - is_default: false, - team: { id: 'team-first', slug: 'first-team' }, - }, - { - team_id: 'team-second', - is_default: false, - team: { id: 'team-second', slug: 'second-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should use first team from list - expect(result).toEqual({ - id: 'team-first', - slug: 'first-team', - }) - }) - - it('should handle team without slug (use ID instead)', async () => { - setupCookies({}) - - // team has no slug (edge case) - setupDatabaseMock([ - { - team_id: 'team-id-only', - is_default: true, - team: { id: 'team-id-only', slug: '' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should fallback to ID when slug is empty - expect(result).toEqual({ - id: 'team-id-only', - slug: 'team-id-only', - }) + it('returns the default slug-backed team when cookies are missing', async () => { + setupCookies({}) + mockListUserTeams.mockResolvedValue({ + ok: true, + data: [ + createTeam({ id: 'team-a', slug: 'team-a' }), + createTeam({ id: 'team-b', slug: 'team-b', isDefault: true }), + ], }) - it('should return null when no cookies and no teams', async () => { - setupCookies({}) - - setupDatabaseMock([]) - - const result = await resolveUserTeam('user-123') + const result = await resolveUserTeam('access-token') - expect(result).toBeNull() + expect(result).toEqual({ + id: 'team-b', + slug: 'team-b', }) - - it('should return null when database returns null', async () => { - setupCookies({}) - - setupDatabaseMock(null) - - const result = await resolveUserTeam('user-123') - - expect(result).toBeNull() + expect(mockCreateUserTeamsRepository).toHaveBeenCalledWith({ + accessToken: 'access-token', }) }) - describe('Partial Cookies', () => { - it('should fall back to database when only team ID cookie exists', async () => { - // only team ID cookie, no slug - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-456', - }) - - setupDatabaseMock([ - { - team_id: 'team-default', - is_default: true, - team: { id: 'team-default', slug: 'default-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // verify: did not check authorization (incomplete cookies) - expect(mockCheckUserTeamAuth).not.toHaveBeenCalled() - - // verify: fell back to database - expect(result).toEqual({ - id: 'team-default', - slug: 'default-team', - }) + it('falls back to the first slug-backed team when the default has no slug', async () => { + setupCookies({}) + mockListUserTeams.mockResolvedValue({ + ok: true, + data: [ + createTeam({ id: 'team-default', slug: '', isDefault: true }), + createTeam({ id: 'team-slugged', slug: 'team-slugged' }), + ], }) - it('should fall back to database when only team slug cookie exists', async () => { - // only team slug cookie, no ID - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'my-team', - }) - - setupDatabaseMock([ - { - team_id: 'team-default', - is_default: true, - team: { id: 'team-default', slug: 'default-team' }, - }, - ]) + const result = await resolveUserTeam('access-token') - const result = await resolveUserTeam('user-123') - - expect(mockCheckUserTeamAuth).not.toHaveBeenCalled() - expect(result).toEqual({ - id: 'team-default', - slug: 'default-team', - }) + expect(result).toEqual({ + id: 'team-slugged', + slug: 'team-slugged', }) }) - describe('Database Errors', () => { - it('should return null when database query fails', async () => { - setupCookies({}) - - setupDatabaseMock(null, { message: 'Database connection failed' }) - - const result = await resolveUserTeam('user-123') - - expect(result).toBeNull() + it('falls back to the repository when cookies are partial', async () => { + setupCookies({ + [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-cookie-id', }) - - it('should fall back to database when authorization check fails with error', async () => { - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-456', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'my-team', - }) - - // authorization check fails (returns false on error) - mockCheckUserTeamAuth.mockResolvedValue(false) - - setupDatabaseMock([ - { - team_id: 'team-default', - is_default: true, - team: { id: 'team-default', slug: 'default-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should still fall back to database - expect(result).toEqual({ - id: 'team-default', - slug: 'default-team', - }) + mockListUserTeams.mockResolvedValue({ + ok: true, + data: [createTeam({ id: 'team-db', slug: 'team-db', isDefault: true })], }) - it('should return null when team relation is malformed', async () => { - setupCookies({}) + const result = await resolveUserTeam('access-token') - // malformed data - team relation is null - setupDatabaseMock([ - { - team_id: 'team-123', - is_default: true, - team: null, // malformed! - }, - ]) - - const result = await resolveUserTeam('user-123') - - expect(result).toBeNull() + expect(result).toEqual({ + id: 'team-db', + slug: 'team-db', }) }) - describe('Complex Scenarios', () => { - it('should prefer default team over first team when both exist', async () => { - setupCookies({}) - - setupDatabaseMock([ - { - team_id: 'team-first', - is_default: false, - team: { id: 'team-first', slug: 'first-team' }, - }, - { - team_id: 'team-default', - is_default: true, - team: { id: 'team-default', slug: 'default-team' }, - }, - { - team_id: 'team-third', - is_default: false, - team: { id: 'team-third', slug: 'third-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should use default team, not first team - expect(result).toEqual({ - id: 'team-default', - slug: 'default-team', - }) + it('returns null when no slug-backed team can be resolved', async () => { + setupCookies({}) + mockListUserTeams.mockResolvedValue({ + ok: true, + data: [ + createTeam({ id: 'team-a', slug: '', isDefault: true }), + createTeam({ id: 'team-b', slug: '' }), + ], }) - it('should handle user switching teams correctly', async () => { - // first call - user has cookies for team A - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-a', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'team-a-slug', - }) - - mockCheckUserTeamAuth.mockResolvedValue(true) - - let result = await resolveUserTeam('user-123') - - expect(result).toEqual({ - id: 'team-a', - slug: 'team-a-slug', - }) - - // second call - user switches to team B - vi.clearAllMocks() - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-b', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'team-b-slug', - }) + const result = await resolveUserTeam('access-token') - mockCheckUserTeamAuth.mockResolvedValue(true) - - result = await resolveUserTeam('user-123') + expect(result).toBeNull() + }) - expect(result).toEqual({ - id: 'team-b', - slug: 'team-b-slug', - }) + it('returns null when listing teams fails', async () => { + setupCookies({}) + mockListUserTeams.mockResolvedValue({ + ok: false, + error: new Error('Failed to fetch user teams'), }) - it('should prevent access when user loses team membership', async () => { - // user has cookies for a team they were previously a member of - setupCookies({ - [COOKIE_KEYS.SELECTED_TEAM_ID]: 'team-removed', - [COOKIE_KEYS.SELECTED_TEAM_SLUG]: 'removed-team', - }) - - // authorization check fails (no longer a member) - mockCheckUserTeamAuth.mockResolvedValue(false) + const result = await resolveUserTeam('access-token') - // user has other teams - setupDatabaseMock([ - { - team_id: 'team-remaining', - is_default: true, - team: { id: 'team-remaining', slug: 'remaining-team' }, - }, - ]) - - const result = await resolveUserTeam('user-123') - - // should redirect to remaining team - expect(result).toEqual({ - id: 'team-remaining', - slug: 'remaining-team', - }) - }) + expect(result).toBeNull() }) }) diff --git a/src/app/api/teams/[teamId]/metrics/route.ts b/src/app/api/teams/[teamSlug]/metrics/route.ts similarity index 75% rename from src/app/api/teams/[teamId]/metrics/route.ts rename to src/app/api/teams/[teamSlug]/metrics/route.ts index 72ae7ba80..97d25690a 100644 --- a/src/app/api/teams/[teamId]/metrics/route.ts +++ b/src/app/api/teams/[teamSlug]/metrics/route.ts @@ -3,15 +3,16 @@ import 'server-cli-only' import { serializeError } from 'serialize-error' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import { getTeamMetricsCore } from '@/core/server/functions/sandboxes/get-team-metrics-core' +import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug' import { l } from '@/core/shared/clients/logger/logger' import { TeamMetricsRequestSchema, type TeamMetricsResponse } from './types' export async function POST( request: Request, - { params }: { params: Promise<{ teamId: string }> } + { params }: { params: Promise<{ teamSlug: string }> } ) { try { - const { teamId } = await params + const { teamSlug } = await params const parsedInput = TeamMetricsRequestSchema.safeParse(await request.json()) @@ -21,7 +22,7 @@ export async function POST( { key: 'team_metrics_route_handler:invalid_request', error: serializeError(parsedInput.error), - team_id: teamId, + team_slug: teamSlug, context: { request: parsedInput.data, }, @@ -40,7 +41,7 @@ export async function POST( l.warn( { key: 'team_metrics_route_handler:unauthenticated', - team_id: teamId, + team_slug: teamSlug, }, 'team_metrics_route_handler: unauthenticated' ) @@ -48,6 +49,21 @@ export async function POST( return Response.json({ error: 'Unauthenticated' }, { status: 401 }) } + const teamId = await getTeamIdFromSlug(teamSlug, session.access_token) + + if (!teamId) { + l.warn( + { + key: 'team_metrics_route_handler:forbidden_team', + team_slug: teamSlug, + user_id: session.user.id, + }, + 'team_metrics_route_handler: forbidden team' + ) + + return Response.json({ error: 'Forbidden' }, { status: 403 }) + } + const result = await getTeamMetricsCore({ accessToken: session.access_token, teamId, diff --git a/src/app/api/teams/[teamId]/metrics/types.ts b/src/app/api/teams/[teamSlug]/metrics/types.ts similarity index 100% rename from src/app/api/teams/[teamId]/metrics/types.ts rename to src/app/api/teams/[teamSlug]/metrics/types.ts diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts b/src/app/api/teams/[teamSlug]/sandboxes/metrics/route.ts similarity index 78% rename from src/app/api/teams/[teamId]/sandboxes/metrics/route.ts rename to src/app/api/teams/[teamSlug]/sandboxes/metrics/route.ts index fed710bec..a17c98ee7 100644 --- a/src/app/api/teams/[teamId]/sandboxes/metrics/route.ts +++ b/src/app/api/teams/[teamSlug]/sandboxes/metrics/route.ts @@ -3,6 +3,7 @@ import 'server-cli-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { handleDefaultInfraError } from '@/core/server/actions/utils' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' +import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug' import { transformMetricsToClientMetrics } from '@/core/server/functions/sandboxes/utils' import { infra } from '@/core/shared/clients/api' import { l } from '@/core/shared/clients/logger/logger' @@ -10,10 +11,10 @@ import { MetricsRequestSchema, type MetricsResponse } from './types' export async function POST( request: Request, - { params }: { params: Promise<{ teamId: string }> } + { params }: { params: Promise<{ teamSlug: string }> } ) { try { - const { teamId } = await params + const { teamSlug } = await params const { success, data } = MetricsRequestSchema.safeParse( await request.json() @@ -32,6 +33,21 @@ export async function POST( return Response.json({ error: 'Unauthenticated' }, { status: 401 }) } + const teamId = await getTeamIdFromSlug(teamSlug, session.access_token) + + if (!teamId) { + l.warn( + { + key: 'get_team_sandboxes_metrics:forbidden_team', + team_slug: teamSlug, + user_id: session.user.id, + }, + 'Failed to resolve team slug for sandbox metrics' + ) + + return Response.json({ error: 'Forbidden' }, { status: 403 }) + } + const infraRes = await infra.GET('/sandboxes/metrics', { params: { query: { diff --git a/src/app/api/teams/[teamId]/sandboxes/metrics/types.ts b/src/app/api/teams/[teamSlug]/sandboxes/metrics/types.ts similarity index 100% rename from src/app/api/teams/[teamId]/sandboxes/metrics/types.ts rename to src/app/api/teams/[teamSlug]/sandboxes/metrics/types.ts diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/page.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/page.tsx deleted file mode 100644 index d42f68b53..000000000 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SandboxLogsView from '@/features/dashboard/sandbox/logs/view' - -export default async function SandboxLogsPage({ - params, -}: { - params: Promise<{ teamIdOrSlug: string; sandboxId: string }> -}) { - const { teamIdOrSlug, sandboxId } = await params - - return -} diff --git a/src/app/dashboard/[teamIdOrSlug]/account/page.tsx b/src/app/dashboard/[teamSlug]/account/page.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/account/page.tsx rename to src/app/dashboard/[teamSlug]/account/page.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/billing/page.tsx b/src/app/dashboard/[teamSlug]/billing/page.tsx similarity index 63% rename from src/app/dashboard/[teamIdOrSlug]/billing/page.tsx rename to src/app/dashboard/[teamSlug]/billing/page.tsx index 72ab6dbd7..4c7111e69 100644 --- a/src/app/dashboard/[teamIdOrSlug]/billing/page.tsx +++ b/src/app/dashboard/[teamSlug]/billing/page.tsx @@ -6,13 +6,13 @@ import { HydrateClient, prefetch, trpc } from '@/trpc/server' export default async function BillingPage({ params, }: { - params: Promise<{ teamIdOrSlug: string }> + params: Promise<{ teamSlug: string }> }) { - const { teamIdOrSlug } = await params + const { teamSlug } = await params - prefetch(trpc.billing.getItems.queryOptions({ teamIdOrSlug })) - prefetch(trpc.billing.getUsage.queryOptions({ teamIdOrSlug })) - prefetch(trpc.billing.getInvoices.queryOptions({ teamIdOrSlug })) + prefetch(trpc.billing.getItems.queryOptions({ teamSlug })) + prefetch(trpc.billing.getUsage.queryOptions({ teamSlug })) + prefetch(trpc.billing.getInvoices.queryOptions({ teamSlug })) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx b/src/app/dashboard/[teamSlug]/billing/plan/page.tsx similarity index 73% rename from src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx rename to src/app/dashboard/[teamSlug]/billing/plan/page.tsx index 2f66aa4c3..ee3f5f824 100644 --- a/src/app/dashboard/[teamIdOrSlug]/billing/plan/page.tsx +++ b/src/app/dashboard/[teamSlug]/billing/plan/page.tsx @@ -5,11 +5,11 @@ import { HydrateClient, prefetch, trpc } from '@/trpc/server' export default async function BillingPlanPage({ params, }: { - params: Promise<{ teamIdOrSlug: string }> + params: Promise<{ teamSlug: string }> }) { - const { teamIdOrSlug } = await params + const { teamSlug } = await params - prefetch(trpc.billing.getItems.queryOptions({ teamIdOrSlug })) + prefetch(trpc.billing.getItems.queryOptions({ teamSlug })) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/billing/plan/select/page.tsx b/src/app/dashboard/[teamSlug]/billing/plan/select/page.tsx similarity index 69% rename from src/app/dashboard/[teamIdOrSlug]/billing/plan/select/page.tsx rename to src/app/dashboard/[teamSlug]/billing/plan/select/page.tsx index a3b3130ce..18a0014bb 100644 --- a/src/app/dashboard/[teamIdOrSlug]/billing/plan/select/page.tsx +++ b/src/app/dashboard/[teamSlug]/billing/plan/select/page.tsx @@ -4,11 +4,11 @@ import { HydrateClient, prefetch, trpc } from '@/trpc/server' export default async function BillingPlanSelectPage({ params, }: { - params: Promise<{ teamIdOrSlug: string }> + params: Promise<{ teamSlug: string }> }) { - const { teamIdOrSlug } = await params + const { teamSlug } = await params - prefetch(trpc.billing.getItems.queryOptions({ teamIdOrSlug })) + prefetch(trpc.billing.getItems.queryOptions({ teamSlug })) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/general/page.tsx b/src/app/dashboard/[teamSlug]/general/page.tsx similarity index 97% rename from src/app/dashboard/[teamIdOrSlug]/general/page.tsx rename to src/app/dashboard/[teamSlug]/general/page.tsx index 75a409635..60519292a 100644 --- a/src/app/dashboard/[teamIdOrSlug]/general/page.tsx +++ b/src/app/dashboard/[teamSlug]/general/page.tsx @@ -5,7 +5,7 @@ import Frame from '@/ui/frame' interface GeneralPageProps { params: Promise<{ - teamIdOrSlug: string + teamSlug: string }> } diff --git a/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx b/src/app/dashboard/[teamSlug]/keys/page.tsx similarity index 98% rename from src/app/dashboard/[teamIdOrSlug]/keys/page.tsx rename to src/app/dashboard/[teamSlug]/keys/page.tsx index 9589fdc82..7acc106ef 100644 --- a/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx +++ b/src/app/dashboard/[teamSlug]/keys/page.tsx @@ -13,7 +13,7 @@ import { interface KeysPageClientProps { params: Promise<{ - teamIdOrSlug: string + teamSlug: string }> } diff --git a/src/app/dashboard/[teamIdOrSlug]/layout.tsx b/src/app/dashboard/[teamSlug]/layout.tsx similarity index 88% rename from src/app/dashboard/[teamIdOrSlug]/layout.tsx rename to src/app/dashboard/[teamSlug]/layout.tsx index ae296f098..eaf3ac776 100644 --- a/src/app/dashboard/[teamIdOrSlug]/layout.tsx +++ b/src/app/dashboard/[teamSlug]/layout.tsx @@ -1,7 +1,7 @@ import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import type { Metadata } from 'next/types' -import { DashboardTeamGate } from '@/app/dashboard/[teamIdOrSlug]/team-gate' +import { DashboardTeamGate } from '@/app/dashboard/[teamSlug]/team-gate' import { COOKIE_KEYS } from '@/configs/cookies' import { METADATA } from '@/configs/metadata' import { AUTH_URLS } from '@/configs/urls' @@ -10,7 +10,7 @@ import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' import DashboardLayoutView from '@/features/dashboard/layouts/layout' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { HydrateClient, prefetch, trpc } from '@/trpc/server' +import { HydrateClient, prefetchAsync, trpc } from '@/trpc/server' import { SidebarInset, SidebarProvider } from '@/ui/primitives/sidebar' export const metadata: Metadata = { @@ -23,7 +23,7 @@ export const metadata: Metadata = { export interface DashboardLayoutProps { params: Promise<{ - teamIdOrSlug: string + teamSlug: string }> children: React.ReactNode } @@ -33,7 +33,7 @@ export default async function DashboardLayout({ params, }: DashboardLayoutProps) { const cookieStore = await cookies() - const { teamIdOrSlug } = await params + const { teamSlug } = await params const session = await getSessionInsecure() const { error, data } = await getUserByToken(session?.access_token) @@ -45,13 +45,13 @@ export default async function DashboardLayout({ throw redirect(AUTH_URLS.SIGN_IN) } - prefetch( + await prefetchAsync( trpc.teams.list.queryOptions(undefined, DASHBOARD_TEAMS_LIST_QUERY_OPTIONS) ) return ( - + diff --git a/src/app/dashboard/[teamIdOrSlug]/limits/loading.tsx b/src/app/dashboard/[teamSlug]/limits/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/limits/loading.tsx rename to src/app/dashboard/[teamSlug]/limits/loading.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/limits/page.tsx b/src/app/dashboard/[teamSlug]/limits/page.tsx similarity index 77% rename from src/app/dashboard/[teamIdOrSlug]/limits/page.tsx rename to src/app/dashboard/[teamSlug]/limits/page.tsx index e27873368..e97b7f7b9 100644 --- a/src/app/dashboard/[teamIdOrSlug]/limits/page.tsx +++ b/src/app/dashboard/[teamSlug]/limits/page.tsx @@ -3,13 +3,13 @@ import { HydrateClient, prefetch, trpc } from '@/trpc/server' import Frame from '@/ui/frame' interface LimitsPageProps { - params: Promise<{ teamIdOrSlug: string }> + params: Promise<{ teamSlug: string }> } export default async function LimitsPage({ params }: LimitsPageProps) { - const { teamIdOrSlug } = await params + const { teamSlug } = await params - prefetch(trpc.billing.getLimits.queryOptions({ teamIdOrSlug })) + prefetch(trpc.billing.getLimits.queryOptions({ teamSlug })) return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/members/page.tsx b/src/app/dashboard/[teamSlug]/members/page.tsx similarity index 95% rename from src/app/dashboard/[teamIdOrSlug]/members/page.tsx rename to src/app/dashboard/[teamSlug]/members/page.tsx index c15f5ba6e..6ce32e938 100644 --- a/src/app/dashboard/[teamIdOrSlug]/members/page.tsx +++ b/src/app/dashboard/[teamSlug]/members/page.tsx @@ -3,7 +3,7 @@ import Frame from '@/ui/frame' interface MembersPageProps { params: Promise<{ - teamIdOrSlug: string + teamSlug: string }> } diff --git a/src/app/dashboard/[teamIdOrSlug]/not-found.tsx b/src/app/dashboard/[teamSlug]/not-found.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/not-found.tsx rename to src/app/dashboard/[teamSlug]/not-found.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@list/default.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@list/default.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@list/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx similarity index 81% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@list/page.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx index 2ecbacc8d..b33a23310 100644 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@list/page.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx @@ -5,12 +5,12 @@ import { HydrateClient, prefetch, trpc } from '@/trpc/server' export default async function ListPage({ params, -}: PageProps<'/dashboard/[teamIdOrSlug]/sandboxes'>) { - const { teamIdOrSlug } = await params +}: PageProps<'/dashboard/[teamSlug]/sandboxes'>) { + const { teamSlug } = await params prefetch( trpc.sandboxes.getSandboxes.queryOptions({ - teamIdOrSlug, + teamSlug, }) ) diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@monitoring/default.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@monitoring/default.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@monitoring/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx similarity index 92% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@monitoring/page.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx index 498cf481b..a80be0b05 100644 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/@monitoring/page.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx @@ -4,7 +4,7 @@ import SandboxesMonitoringHeader from '@/features/dashboard/sandboxes/monitoring export default async function MonitoringPage({ params, searchParams, -}: PageProps<'/dashboard/[teamIdOrSlug]/sandboxes'> & { +}: PageProps<'/dashboard/[teamSlug]/sandboxes'> & { searchParams: Promise<{ start?: string; end?: string }> }) { return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/layout.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx similarity index 92% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/layout.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx index bb7d6a5ce..ef434b39e 100644 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/layout.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx @@ -4,7 +4,7 @@ import { ListIcon, TrendIcon } from '@/ui/primitives/icons' export default function SandboxesLayout({ list, monitoring, -}: LayoutProps<'/dashboard/[teamIdOrSlug]/sandboxes'> & { +}: LayoutProps<'/dashboard/[teamSlug]/sandboxes'> & { list: React.ReactNode monitoring: React.ReactNode }) { diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/(tabs)/page.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/filesystem/loading.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/filesystem/loading.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/loading.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/filesystem/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/page.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/filesystem/page.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/page.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/layout.tsx similarity index 85% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/layout.tsx index 8ee3d6715..238eb10af 100644 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/layout.tsx @@ -6,18 +6,18 @@ import { prefetch, trpc } from '@/trpc/server' interface SandboxLayoutProps { children: React.ReactNode - params: Promise<{ teamIdOrSlug: string; sandboxId: string }> + params: Promise<{ teamSlug: string; sandboxId: string }> } export default async function SandboxLayout({ children, params, }: SandboxLayoutProps) { - const { teamIdOrSlug, sandboxId } = await params + const { teamSlug, sandboxId } = await params prefetch( trpc.sandbox.details.queryOptions( - { teamIdOrSlug, sandboxId }, + { teamSlug, sandboxId }, { retry: false, staleTime: Number.POSITIVE_INFINITY, diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/loading.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/logs/loading.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/loading.tsx diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/page.tsx new file mode 100644 index 000000000..8b352b588 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/page.tsx @@ -0,0 +1,11 @@ +import SandboxLogsView from '@/features/dashboard/sandbox/logs/view' + +export default async function SandboxLogsPage({ + params, +}: { + params: Promise<{ teamSlug: string; sandboxId: string }> +}) { + const { teamSlug, sandboxId } = await params + + return +} diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/monitoring/loading.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/monitoring/loading.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/loading.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/monitoring/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/page.tsx similarity index 81% rename from src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/monitoring/page.tsx rename to src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/page.tsx index 2b0dbc4b2..4d8755ee6 100644 --- a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/monitoring/page.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/page.tsx @@ -3,7 +3,7 @@ import SandboxMonitoringView from '@/features/dashboard/sandbox/monitoring/compo export default async function SandboxMonitoringPage({ params, }: { - params: Promise<{ teamIdOrSlug: string; sandboxId: string }> + params: Promise<{ teamSlug: string; sandboxId: string }> }) { const { sandboxId } = await params diff --git a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx b/src/app/dashboard/[teamSlug]/team-gate.tsx similarity index 85% rename from src/app/dashboard/[teamIdOrSlug]/team-gate.tsx rename to src/app/dashboard/[teamSlug]/team-gate.tsx index ad0a4062e..b9aaceea9 100644 --- a/src/app/dashboard/[teamIdOrSlug]/team-gate.tsx +++ b/src/app/dashboard/[teamSlug]/team-gate.tsx @@ -13,22 +13,19 @@ import { useTRPC } from '@/trpc/client' import Unauthorized from '../unauthorized' interface DashboardTeamGateProps { - teamIdOrSlug: string + teamSlug: string user: User children: React.ReactNode } -function TeamContent({ teamIdOrSlug, user, children }: DashboardTeamGateProps) { +function TeamContent({ teamSlug, user, children }: DashboardTeamGateProps) { const trpc = useTRPC() const { data: teams } = useSuspenseQuery( trpc.teams.list.queryOptions(undefined, DASHBOARD_TEAMS_LIST_QUERY_OPTIONS) ) - const team = teams.find( - (candidate) => - candidate.id === teamIdOrSlug || candidate.slug === teamIdOrSlug - ) + const team = teams.find((candidate) => candidate.slug === teamSlug) if (!team) { throw new Error('Team not found or access denied') diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/default.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/default.tsx rename to src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/page.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@builds/page.tsx rename to src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/default.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/default.tsx rename to src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/page.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/@list/page.tsx rename to src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx similarity index 92% rename from src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx rename to src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx index 4e7debefb..292e65e25 100644 --- a/src/app/dashboard/[teamIdOrSlug]/templates/(tabs)/layout.tsx +++ b/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx @@ -4,7 +4,7 @@ import { BuildIcon, ListIcon } from '@/ui/primitives/icons' export default function TemplatesLayout({ list, builds, -}: LayoutProps<'/dashboard/[teamIdOrSlug]/templates'> & { +}: LayoutProps<'/dashboard/[teamSlug]/templates'> & { list: React.ReactNode builds: React.ReactNode }) { diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]/loading.tsx b/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]/loading.tsx rename to src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/loading.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]/page.tsx b/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/page.tsx similarity index 87% rename from src/app/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]/page.tsx rename to src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/page.tsx index 6866a84d1..3dd525bce 100644 --- a/src/app/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]/page.tsx +++ b/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/page.tsx @@ -12,8 +12,8 @@ const REFETCH_INTERVAL_MS = 1_500 export default function BuildPage({ params, -}: PageProps<'/dashboard/[teamIdOrSlug]/templates/[templateId]/builds/[buildId]'>) { - const { teamIdOrSlug, templateId, buildId } = use(params) +}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]'>) { + const { teamSlug, templateId, buildId } = use(params) const trpc = useTRPC() const { @@ -22,7 +22,7 @@ export default function BuildPage({ isPending, } = useQuery( trpc.builds.buildDetails.queryOptions( - { teamIdOrSlug, templateId, buildId }, + { teamSlug, templateId, buildId }, { refetchIntervalInBackground: false, refetchOnWindowFocus: ({ state }) => @@ -55,7 +55,7 @@ export default function BuildPage({ /> diff --git a/src/app/dashboard/[teamIdOrSlug]/usage/loading.tsx b/src/app/dashboard/[teamSlug]/usage/loading.tsx similarity index 100% rename from src/app/dashboard/[teamIdOrSlug]/usage/loading.tsx rename to src/app/dashboard/[teamSlug]/usage/loading.tsx diff --git a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx b/src/app/dashboard/[teamSlug]/usage/page.tsx similarity index 94% rename from src/app/dashboard/[teamIdOrSlug]/usage/page.tsx rename to src/app/dashboard/[teamSlug]/usage/page.tsx index 6b3f56b07..aa80e4d88 100644 --- a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx +++ b/src/app/dashboard/[teamSlug]/usage/page.tsx @@ -7,10 +7,10 @@ import Frame from '@/ui/frame' export default async function UsagePage({ params, }: { - params: Promise<{ teamIdOrSlug: string }> + params: Promise<{ teamSlug: string }> }) { - const { teamIdOrSlug } = await params - const result = await getUsage({ teamIdOrSlug }) + const { teamSlug } = await params + const result = await getUsage({ teamSlug }) if (!result?.data || result.serverError || result.validationErrors) { return ( diff --git a/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/page.tsx similarity index 98% rename from src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx rename to src/app/dashboard/[teamSlug]/webhooks/page.tsx index 911f43ab1..11146c781 100644 --- a/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/page.tsx @@ -14,7 +14,7 @@ import { interface WebhooksPageClientProps { params: Promise<{ - teamIdOrSlug: string + teamSlug: string }> } diff --git a/src/app/dashboard/account/route.ts b/src/app/dashboard/account/route.ts index e0be135c4..0267ce345 100644 --- a/src/app/dashboard/account/route.ts +++ b/src/app/dashboard/account/route.ts @@ -36,9 +36,7 @@ export async function GET(request: NextRequest) { await setTeamCookies(team.id, team.slug) - const redirectPath = PROTECTED_URLS.RESOLVED_ACCOUNT_SETTINGS( - team.slug || team.id - ) + const redirectPath = PROTECTED_URLS.RESOLVED_ACCOUNT_SETTINGS(team.slug) const redirectUrl = new URL(redirectPath, request.url) request.nextUrl.searchParams.forEach((value, key) => { diff --git a/src/app/dashboard/route.ts b/src/app/dashboard/route.ts index 26e522391..13f082785 100644 --- a/src/app/dashboard/route.ts +++ b/src/app/dashboard/route.ts @@ -6,21 +6,21 @@ import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { setTeamCookies } from '@/lib/utils/cookies' -export const TAB_URL_MAP: Record string> = { - sandboxes: (teamId) => PROTECTED_URLS.SANDBOXES(teamId), - templates: (teamId) => PROTECTED_URLS.TEMPLATES(teamId), - usage: (teamId) => PROTECTED_URLS.USAGE(teamId), - billing: (teamId) => PROTECTED_URLS.BILLING(teamId), - limits: (teamId) => PROTECTED_URLS.LIMITS(teamId), - keys: (teamId) => PROTECTED_URLS.KEYS(teamId), - settings: (teamId) => PROTECTED_URLS.GENERAL(teamId), - team: (teamId) => PROTECTED_URLS.GENERAL(teamId), - general: (teamId) => PROTECTED_URLS.GENERAL(teamId), - members: (teamId) => PROTECTED_URLS.MEMBERS(teamId), +export const TAB_URL_MAP: Record string> = { + sandboxes: (teamSlug) => PROTECTED_URLS.SANDBOXES(teamSlug), + templates: (teamSlug) => PROTECTED_URLS.TEMPLATES(teamSlug), + usage: (teamSlug) => PROTECTED_URLS.USAGE(teamSlug), + billing: (teamSlug) => PROTECTED_URLS.BILLING(teamSlug), + limits: (teamSlug) => PROTECTED_URLS.LIMITS(teamSlug), + keys: (teamSlug) => PROTECTED_URLS.KEYS(teamSlug), + settings: (teamSlug) => PROTECTED_URLS.GENERAL(teamSlug), + team: (teamSlug) => PROTECTED_URLS.GENERAL(teamSlug), + general: (teamSlug) => PROTECTED_URLS.GENERAL(teamSlug), + members: (teamSlug) => PROTECTED_URLS.MEMBERS(teamSlug), account: (_) => PROTECTED_URLS.ACCOUNT_SETTINGS, personal: (_) => PROTECTED_URLS.ACCOUNT_SETTINGS, - budget: (teamId) => PROTECTED_URLS.LIMITS(teamId), + budget: (teamSlug) => PROTECTED_URLS.LIMITS(teamSlug), } export async function GET(request: NextRequest) { @@ -59,8 +59,8 @@ export async function GET(request: NextRequest) { const urlGenerator = tab ? TAB_URL_MAP[tab] : null const redirectPath = urlGenerator - ? urlGenerator(team.slug || team.id) - : PROTECTED_URLS.SANDBOXES(team.slug || team.id) + ? urlGenerator(team.slug) + : PROTECTED_URLS.SANDBOXES(team.slug) const redirectUrl = new URL(redirectPath, request.url) diff --git a/src/configs/cache.ts b/src/configs/cache.ts index 35694d9c6..5a484272f 100644 --- a/src/configs/cache.ts +++ b/src/configs/cache.ts @@ -1,7 +1,7 @@ export const CACHE_TAGS = { USER_TEAMS: (userId: string) => `user-teams-${userId}`, - TEAM_ID_FROM_SEGMENT: (segment: string) => `team-id-from-segment-${segment}`, + TEAM_ID_FROM_SLUG: (segment: string) => `team-id-from-slug-${segment}`, TEAM_TEMPLATES: (teamId: string) => `team-templates-${teamId}`, TEAM_SANDBOXES_LIST: (teamId: string) => `team-sandboxes-list-${teamId}`, TEAM_USAGE: (teamId: string) => `team-usage-${teamId}`, diff --git a/src/configs/keys.ts b/src/configs/keys.ts index 901f99109..cb602e32c 100644 --- a/src/configs/keys.ts +++ b/src/configs/keys.ts @@ -3,8 +3,8 @@ * Note: Cookie keys have been moved to @/configs/cookies */ export const KV_KEYS = { - USER_TEAM_ACCESS: (userId: string, teamIdOrSlug: string) => - `user-team-access:${userId}:${teamIdOrSlug}`, + USER_TEAM_ACCESS: (userId: string, teamSlug: string) => + `user-team-access:${userId}:${teamSlug}`, TEAM_SLUG_TO_ID: (slug: string) => `team-slug:${slug}:id`, TEAM_ID_TO_SLUG: (teamId: string) => `team-id:${teamId}:slug`, WARNED_ALTERNATE_EMAIL: (email: string) => `warned-alternate-email:${email}`, @@ -15,20 +15,26 @@ export const KV_KEYS = { */ export const SWR_KEYS = { // team metrics keys - all components using the same key share the same cache - TEAM_METRICS_RECENT: (teamId: string) => - [`/api/teams/${teamId}/metrics`, teamId, 'recent'] as const, - TEAM_METRICS_MONITORING: (teamId: string, start: number, end: number) => - [`/api/teams/${teamId}/metrics`, teamId, 'monitoring', start, end] as const, - TEAM_METRICS_HISTORICAL: (teamId: string, days: number) => - [`/api/teams/${teamId}/metrics`, teamId, 'historical', days] as const, + TEAM_METRICS_RECENT: (teamSlug: string) => + [`/api/teams/${teamSlug}/metrics`, teamSlug, 'recent'] as const, + TEAM_METRICS_MONITORING: (teamSlug: string, start: number, end: number) => + [ + `/api/teams/${teamSlug}/metrics`, + teamSlug, + 'monitoring', + start, + end, + ] as const, + TEAM_METRICS_HISTORICAL: (teamSlug: string, days: number) => + [`/api/teams/${teamSlug}/metrics`, teamSlug, 'historical', days] as const, // sandbox metrics keys - SANDBOX_METRICS: (teamId: string, sandboxIds: string[]) => - [`/api/teams/${teamId}/sandboxes/metrics`, sandboxIds] as const, + SANDBOX_METRICS: (teamSlug: string, sandboxIds: string[]) => + [`/api/teams/${teamSlug}/sandboxes/metrics`, sandboxIds] as const, SANDBOX_INFO: (sandboxId: string) => [`/api/sandbox/details`, sandboxId] as const, // sandboxes list keys - SANDBOXES_LIST: (teamId: string) => - [`/api/teams/${teamId}/sandboxes/list`] as const, + SANDBOXES_LIST: (teamSlug: string) => + [`/api/teams/${teamSlug}/sandboxes/list`] as const, } diff --git a/src/configs/layout.ts b/src/configs/layout.ts index 3c32893fe..c8414a2cc 100644 --- a/src/configs/layout.ts +++ b/src/configs/layout.ts @@ -30,7 +30,7 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< }), '/dashboard/*/sandboxes/*/*': (pathname) => { const parts = pathname.split('/') - const teamIdOrSlug = parts[2]! + const teamSlug = parts[2]! const sandboxId = parts[4]! const sandboxIdSliced = `${sandboxId.slice(0, 6)}...${sandboxId.slice(-6)}` @@ -38,7 +38,7 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< title: [ { label: 'Sandboxes', - href: PROTECTED_URLS.SANDBOXES_LIST(teamIdOrSlug), + href: PROTECTED_URLS.SANDBOXES_LIST(teamSlug), }, { label: sandboxIdSliced }, ], @@ -55,7 +55,7 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< }), '/dashboard/*/templates/*/builds/*': (pathname) => { const parts = pathname.split('/') - const teamIdOrSlug = parts[2]! + const teamSlug = parts[2]! const buildId = parts.pop()! const buildIdSliced = `${buildId.slice(0, 6)}...${buildId.slice(-6)}` @@ -63,7 +63,7 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< title: [ { label: 'Templates', - href: PROTECTED_URLS.TEMPLATES_BUILDS(teamIdOrSlug), + href: PROTECTED_URLS.TEMPLATES_BUILDS(teamSlug), }, { label: `Build ${buildIdSliced}` }, ], @@ -113,11 +113,11 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< }), '/dashboard/*/billing/plan': (pathname) => { const parts = pathname.split('/') - const teamIdOrSlug = parts[2]! + const teamSlug = parts[2]! return { title: [ - { label: 'Billing', href: PROTECTED_URLS.BILLING(teamIdOrSlug) }, + { label: 'Billing', href: PROTECTED_URLS.BILLING(teamSlug) }, { label: 'Plan & Add-ons', }, @@ -127,14 +127,14 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< }, '/dashboard/*/billing/plan/select': (pathname) => { const parts = pathname.split('/') - const teamIdOrSlug = parts[2]! + const teamSlug = parts[2]! return { title: [ - { label: 'Billing', href: PROTECTED_URLS.BILLING(teamIdOrSlug) }, + { label: 'Billing', href: PROTECTED_URLS.BILLING(teamSlug) }, { label: 'Plan & Add-ons', - href: PROTECTED_URLS.BILLING_PLAN(teamIdOrSlug), + href: PROTECTED_URLS.BILLING_PLAN(teamSlug), }, { label: 'Change Plan' }, ], diff --git a/src/configs/mock-data.ts b/src/configs/mock-data.ts index 60d962e7e..3c3cd9817 100644 --- a/src/configs/mock-data.ts +++ b/src/configs/mock-data.ts @@ -1,6 +1,6 @@ import { addHours, subHours } from 'date-fns' import { nanoid } from 'nanoid' -import type { MetricsResponse } from '@/app/api/teams/[teamId]/sandboxes/metrics/types' +import type { MetricsResponse } from '@/app/api/teams/[teamSlug]/sandboxes/metrics/types' import type { ClientSandboxesMetrics, ClientTeamMetrics, diff --git a/src/configs/sidebar.ts b/src/configs/sidebar.ts index 65bf013f7..64f6102d6 100644 --- a/src/configs/sidebar.ts +++ b/src/configs/sidebar.ts @@ -15,7 +15,7 @@ import { INCLUDE_ARGUS, INCLUDE_BILLING } from './flags' import { PROTECTED_URLS } from './urls' type SidebarNavArgs = { - teamIdOrSlug?: string + teamSlug?: string } export type SidebarNavItem = { @@ -34,13 +34,13 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ // Base { label: 'Sandboxes', - href: (args) => PROTECTED_URLS.SANDBOXES(args.teamIdOrSlug!), + href: (args) => PROTECTED_URLS.SANDBOXES(args.teamSlug!), icon: Box, activeMatch: `/dashboard/*/sandboxes/**`, }, { label: 'Templates', - href: (args) => PROTECTED_URLS.TEMPLATES(args.teamIdOrSlug!), + href: (args) => PROTECTED_URLS.TEMPLATES(args.teamSlug!), icon: Container, activeMatch: `/dashboard/*/templates/**`, }, @@ -52,7 +52,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ label: 'Webhooks', group: 'integration', href: (args: SidebarNavArgs) => - PROTECTED_URLS.WEBHOOKS(args.teamIdOrSlug!), + PROTECTED_URLS.WEBHOOKS(args.teamSlug!), icon: WebhookIcon, activeMatch: `/dashboard/*/webhooks`, }, @@ -62,21 +62,21 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ // Team { label: 'General', - href: (args) => PROTECTED_URLS.GENERAL(args.teamIdOrSlug!), + href: (args) => PROTECTED_URLS.GENERAL(args.teamSlug!), icon: Settings, group: 'team', activeMatch: `/dashboard/*/general`, }, { label: 'API Keys', - href: (args) => PROTECTED_URLS.KEYS(args.teamIdOrSlug!), + href: (args) => PROTECTED_URLS.KEYS(args.teamSlug!), icon: Key, group: 'team', activeMatch: `/dashboard/*/keys`, }, { label: 'Members', - href: (args) => PROTECTED_URLS.MEMBERS(args.teamIdOrSlug!), + href: (args) => PROTECTED_URLS.MEMBERS(args.teamSlug!), icon: Users, group: 'team', activeMatch: `/dashboard/*/members`, @@ -87,16 +87,14 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ ? [ { label: 'Usage', - href: (args: SidebarNavArgs) => - PROTECTED_URLS.USAGE(args.teamIdOrSlug!), + href: (args: SidebarNavArgs) => PROTECTED_URLS.USAGE(args.teamSlug!), icon: Activity, group: 'billing', activeMatch: `/dashboard/*/usage/**`, }, { label: 'Limits', - href: (args: SidebarNavArgs) => - PROTECTED_URLS.LIMITS(args.teamIdOrSlug!), + href: (args: SidebarNavArgs) => PROTECTED_URLS.LIMITS(args.teamSlug!), group: 'billing', icon: GaugeIcon, activeMatch: `/dashboard/*/limits/**`, @@ -104,7 +102,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ { label: 'Billing', href: (args: SidebarNavArgs) => - PROTECTED_URLS.BILLING(args.teamIdOrSlug!), + PROTECTED_URLS.BILLING(args.teamSlug!), icon: CreditCard, group: 'billing', activeMatch: `/dashboard/*/billing/**`, diff --git a/src/configs/urls.ts b/src/configs/urls.ts index 5e07610cc..0c6c576e8 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -14,46 +14,45 @@ export const PROTECTED_URLS = { NEW_TEAM: '/dashboard/teams/new', TEAMS: '/dashboard/teams', - RESOLVED_ACCOUNT_SETTINGS: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/account`, + RESOLVED_ACCOUNT_SETTINGS: (teamSlug: string) => + `/dashboard/${teamSlug}/account`, - GENERAL: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/general`, - KEYS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/keys`, - MEMBERS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/members`, + GENERAL: (teamSlug: string) => `/dashboard/${teamSlug}/general`, + KEYS: (teamSlug: string) => `/dashboard/${teamSlug}/keys`, + MEMBERS: (teamSlug: string) => `/dashboard/${teamSlug}/members`, - SANDBOXES: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/sandboxes?tab=monitoring`, - SANDBOXES_MONITORING: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/sandboxes?tab=monitoring`, - SANDBOXES_LIST: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/sandboxes?tab=list`, + SANDBOXES: (teamSlug: string) => + `/dashboard/${teamSlug}/sandboxes?tab=monitoring`, + SANDBOXES_MONITORING: (teamSlug: string) => + `/dashboard/${teamSlug}/sandboxes?tab=monitoring`, + SANDBOXES_LIST: (teamSlug: string) => + `/dashboard/${teamSlug}/sandboxes?tab=list`, - SANDBOX: (teamIdOrSlug: string, sandboxId: string) => - `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/monitoring`, - SANDBOX_MONITORING: (teamIdOrSlug: string, sandboxId: string) => - `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/monitoring`, - SANDBOX_LOGS: (teamIdOrSlug: string, sandboxId: string) => - `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/logs`, - SANDBOX_FILESYSTEM: (teamIdOrSlug: string, sandboxId: string) => - `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/filesystem`, + SANDBOX: (teamSlug: string, sandboxId: string) => + `/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`, + SANDBOX_MONITORING: (teamSlug: string, sandboxId: string) => + `/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`, + SANDBOX_LOGS: (teamSlug: string, sandboxId: string) => + `/dashboard/${teamSlug}/sandboxes/${sandboxId}/logs`, + SANDBOX_FILESYSTEM: (teamSlug: string, sandboxId: string) => + `/dashboard/${teamSlug}/sandboxes/${sandboxId}/filesystem`, - WEBHOOKS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/webhooks`, + WEBHOOKS: (teamSlug: string) => `/dashboard/${teamSlug}/webhooks`, - TEMPLATES: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/templates`, - TEMPLATES_LIST: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/templates?tab=list`, - TEMPLATES_BUILDS: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/templates?tab=builds`, - TEMPLATE_BUILD: (teamIdOrSlug: string, templateId: string, buildId: string) => - `/dashboard/${teamIdOrSlug}/templates/${templateId}/builds/${buildId}`, + TEMPLATES: (teamSlug: string) => `/dashboard/${teamSlug}/templates`, + TEMPLATES_LIST: (teamSlug: string) => + `/dashboard/${teamSlug}/templates?tab=list`, + TEMPLATES_BUILDS: (teamSlug: string) => + `/dashboard/${teamSlug}/templates?tab=builds`, + TEMPLATE_BUILD: (teamSlug: string, templateId: string, buildId: string) => + `/dashboard/${teamSlug}/templates/${templateId}/builds/${buildId}`, - USAGE: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/usage`, - BILLING: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/billing`, - BILLING_PLAN: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/billing/plan`, - BILLING_PLAN_SELECT: (teamIdOrSlug: string) => - `/dashboard/${teamIdOrSlug}/billing/plan/select`, - LIMITS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/limits`, + USAGE: (teamSlug: string) => `/dashboard/${teamSlug}/usage`, + BILLING: (teamSlug: string) => `/dashboard/${teamSlug}/billing`, + BILLING_PLAN: (teamSlug: string) => `/dashboard/${teamSlug}/billing/plan`, + BILLING_PLAN_SELECT: (teamSlug: string) => + `/dashboard/${teamSlug}/billing/plan/select`, + LIMITS: (teamSlug: string) => `/dashboard/${teamSlug}/limits`, } export const RESOLVER_URLS = { diff --git a/src/core/modules/support/repository.server.ts b/src/core/modules/support/repository.server.ts index a0c368c17..12bfa8946 100644 --- a/src/core/modules/support/repository.server.ts +++ b/src/core/modules/support/repository.server.ts @@ -150,7 +150,7 @@ export function createSupportRepository( if (!team) { return err( repoErrorFromHttp(403, 'Team not found or access denied', { - teamIdOrSlug: scope.teamId, + teamId: scope.teamId, }) ) } diff --git a/src/core/modules/teams/schemas.ts b/src/core/modules/teams/schemas.ts index cce34bf2f..91ea1d39f 100644 --- a/src/core/modules/teams/schemas.ts +++ b/src/core/modules/teams/schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' -export { TeamIdOrSlugSchema } +export { TeamSlugSchema } export const TeamNameSchema = z .string() @@ -14,7 +14,7 @@ export const TeamNameSchema = z }) export const UpdateTeamNameSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, name: TeamNameSchema, }) diff --git a/src/core/server/actions/client.ts b/src/core/server/actions/client.ts index 136da7fc2..39ae7e616 100644 --- a/src/core/server/actions/client.ts +++ b/src/core/server/actions/client.ts @@ -6,7 +6,7 @@ import { serializeError } from 'serialize-error' import { z } from 'zod' import { getSessionInsecure } from '@/core/server/functions/auth/get-session' import getUserByToken from '@/core/server/functions/auth/get-user-by-token' -import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' +import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug' import { l } from '@/core/shared/clients/logger/logger' import { createClient } from '@/core/shared/clients/supabase/server' import { getTracer } from '@/core/shared/clients/tracer' @@ -180,44 +180,43 @@ export const authActionClient = actionClient.use(async ({ next }) => { }) }) -export const withTeamIdResolution = createMiddleware<{ +export const withTeamSlugResolution = createMiddleware<{ ctx: AuthActionContext }>().define(async ({ next, clientInput, ctx }) => { if ( !clientInput || typeof clientInput !== 'object' || - !('teamIdOrSlug' in clientInput) + !('teamSlug' in clientInput) ) { l.error( { - key: 'with_team_id_resolution:missing_team_id_or_slug', + key: 'with_team_slug_resolution:missing_team_slug', context: { - teamIdOrSlug: (clientInput as { teamIdOrSlug?: string }) - ?.teamIdOrSlug, + teamSlug: (clientInput as { teamSlug?: string })?.teamSlug, }, }, - 'Missing teamIdOrSlug when using withTeamIdResolution middleware' + 'Missing teamSlug when using withTeamSlugResolution middleware' ) throw new Error( - 'teamIdOrSlug is required when using withTeamIdResolution middleware' + 'teamSlug is required when using withTeamSlugResolution middleware' ) } - const teamId = await getTeamIdFromSegment( - clientInput.teamIdOrSlug as string, + const teamId = await getTeamIdFromSlug( + clientInput.teamSlug as string, ctx.session.access_token ) if (!teamId) { l.warn( { - key: 'with_team_id_resolution:invalid_team_id_or_slug', + key: 'with_team_slug_resolution:invalid_team_slug', context: { - teamIdOrSlug: clientInput.teamIdOrSlug, + teamSlug: clientInput.teamSlug, }, }, - `with_team_id_resolution:invalid_team_id_or_slug - invalid team id or slug provided through withTeamIdResolution middleware: ${clientInput.teamIdOrSlug}` + `with_team_slug_resolution:invalid_team_slug - invalid team slug provided through withTeamSlugResolution middleware: ${clientInput.teamSlug}` ) throw unauthorized() diff --git a/src/core/server/actions/key-actions.ts b/src/core/server/actions/key-actions.ts index fa8bafe63..5c5c1bb73 100644 --- a/src/core/server/actions/key-actions.ts +++ b/src/core/server/actions/key-actions.ts @@ -7,11 +7,11 @@ import { createKeysRepository } from '@/core/modules/keys/repository.server' import { authActionClient, withTeamAuthedRequestRepository, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { returnServerError } from '@/core/server/actions/utils' import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const withKeysRepository = withTeamAuthedRequestRepository( createKeysRepository, @@ -23,7 +23,7 @@ const withKeysRepository = withTeamAuthedRequestRepository( // Create API Key const CreateApiKeySchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, name: z .string({ error: 'Name is required' }) .min(1, 'Name cannot be empty') @@ -34,7 +34,7 @@ const CreateApiKeySchema = z.object({ export const createApiKeyAction = authActionClient .schema(CreateApiKeySchema) .metadata({ actionName: 'createApiKey' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withKeysRepository) .action(async ({ parsedInput, ctx }) => { const { name } = parsedInput @@ -56,8 +56,8 @@ export const createApiKeyAction = authActionClient return returnServerError('Failed to create API Key') } - updateTag(CACHE_TAGS.TEAM_API_KEYS(parsedInput.teamIdOrSlug)) - revalidatePath(`/dashboard/${parsedInput.teamIdOrSlug}/keys`, 'page') + updateTag(CACHE_TAGS.TEAM_API_KEYS(ctx.teamId)) + revalidatePath(`/dashboard/${parsedInput.teamSlug}/keys`, 'page') return { createdApiKey: result.data, @@ -67,14 +67,14 @@ export const createApiKeyAction = authActionClient // Delete API Key const DeleteApiKeySchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, apiKeyId: z.uuid(), }) export const deleteApiKeyAction = authActionClient .schema(DeleteApiKeySchema) .metadata({ actionName: 'deleteApiKey' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withKeysRepository) .action(async ({ parsedInput, ctx }) => { const { apiKeyId } = parsedInput @@ -95,6 +95,6 @@ export const deleteApiKeyAction = authActionClient return returnServerError('Failed to delete API Key') } - updateTag(CACHE_TAGS.TEAM_API_KEYS(parsedInput.teamIdOrSlug)) - revalidatePath(`/dashboard/${parsedInput.teamIdOrSlug}/keys`, 'page') + updateTag(CACHE_TAGS.TEAM_API_KEYS(ctx.teamId)) + revalidatePath(`/dashboard/${parsedInput.teamSlug}/keys`, 'page') }) diff --git a/src/core/server/actions/sandbox-actions.ts b/src/core/server/actions/sandbox-actions.ts index 18ffcd785..dd64879e6 100644 --- a/src/core/server/actions/sandbox-actions.ts +++ b/src/core/server/actions/sandbox-actions.ts @@ -6,22 +6,22 @@ import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { CACHE_TAGS } from '@/configs/cache' import { authActionClient, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { returnServerError } from '@/core/server/actions/utils' import { infra } from '@/core/shared/clients/api' import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const KillSandboxSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, sandboxId: z.string().min(1, 'Sandbox ID is required'), }) export const killSandboxAction = authActionClient .schema(KillSandboxSchema) .metadata({ actionName: 'killSandbox' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .action(async ({ parsedInput, ctx }) => { const { sandboxId } = parsedInput const { session, teamId } = ctx @@ -63,15 +63,13 @@ export const killSandboxAction = authActionClient }) const RevalidateSandboxesSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) export const revalidateSandboxes = authActionClient .metadata({ serverFunctionName: 'revalidateSandboxes' }) .inputSchema(RevalidateSandboxesSchema) - .use(withTeamIdResolution) - .action(async ({ parsedInput }) => { - const { teamIdOrSlug } = parsedInput - - updateTag(CACHE_TAGS.TEAM_SANDBOXES_LIST(teamIdOrSlug)) + .use(withTeamSlugResolution) + .action(async ({ ctx }) => { + updateTag(CACHE_TAGS.TEAM_SANDBOXES_LIST(ctx.teamId)) }) diff --git a/src/core/server/actions/team-actions.ts b/src/core/server/actions/team-actions.ts index 0640f710b..18326dcd4 100644 --- a/src/core/server/actions/team-actions.ts +++ b/src/core/server/actions/team-actions.ts @@ -17,7 +17,7 @@ import { createTeamsRepository } from '@/core/modules/teams/teams-repository.ser import { authActionClient, withTeamAuthedRequestRepository, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { handleDefaultInfraError, @@ -26,7 +26,7 @@ import { import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' import { l } from '@/core/shared/clients/logger/logger' import { deleteFile, getFiles, uploadFile } from '@/core/shared/clients/storage' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const withTeamsRepository = withTeamAuthedRequestRepository( createTeamsRepository, @@ -36,61 +36,61 @@ const withTeamsRepository = withTeamAuthedRequestRepository( export const updateTeamNameAction = authActionClient .schema(UpdateTeamNameSchema) .metadata({ actionName: 'updateTeamName' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { - const { name, teamIdOrSlug } = parsedInput + const { name, teamSlug } = parsedInput const result = await ctx.teamsRepository.updateTeamName(name) if (!result.ok) { return toActionErrorFromRepoError(result.error) } - revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') + revalidatePath(`/dashboard/${teamSlug}/general`, 'page') return result.data }) const AddTeamMemberSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, email: z.email(), }) export const addTeamMemberAction = authActionClient .schema(AddTeamMemberSchema) .metadata({ actionName: 'addTeamMember' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { - const { email, teamIdOrSlug } = parsedInput + const { email, teamSlug } = parsedInput const result = await ctx.teamsRepository.addTeamMember(email) if (!result.ok) { return toActionErrorFromRepoError(result.error) } - revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') + revalidatePath(`/dashboard/${teamSlug}/general`, 'page') }) const RemoveTeamMemberSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, userId: z.uuid(), }) export const removeTeamMemberAction = authActionClient .schema(RemoveTeamMemberSchema) .metadata({ actionName: 'removeTeamMember' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { - const { userId, teamIdOrSlug } = parsedInput + const { userId, teamSlug } = parsedInput const result = await ctx.teamsRepository.removeTeamMember(userId) if (!result.ok) { return toActionErrorFromRepoError(result.error) } - revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') + revalidatePath(`/dashboard/${teamSlug}/general`, 'page') }) export const createTeamAction = authActionClient @@ -127,7 +127,7 @@ export const createTeamAction = authActionClient const UploadTeamProfilePictureSchema = zfd.formData( z.object({ - teamIdOrSlug: zfd.text(), + teamSlug: zfd.text(), image: zfd.file(), }) ) @@ -135,10 +135,10 @@ const UploadTeamProfilePictureSchema = zfd.formData( export const uploadTeamProfilePictureAction = authActionClient .schema(UploadTeamProfilePictureSchema) .metadata({ actionName: 'uploadTeamProfilePicture' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withTeamsRepository) .action(async ({ parsedInput, ctx }) => { - const { image, teamIdOrSlug } = parsedInput + const { image, teamSlug } = parsedInput const { teamId, teamsRepository } = ctx const allowedTypes = ['image/jpeg', 'image/png'] @@ -217,7 +217,7 @@ export const uploadTeamProfilePictureAction = authActionClient } }) - revalidatePath(`/dashboard/${teamIdOrSlug}/general`, 'page') + revalidatePath(`/dashboard/${teamSlug}/general`, 'page') return result.data }) diff --git a/src/core/server/actions/webhooks-actions.ts b/src/core/server/actions/webhooks-actions.ts index b939ace7b..6568e41da 100644 --- a/src/core/server/actions/webhooks-actions.ts +++ b/src/core/server/actions/webhooks-actions.ts @@ -1,13 +1,11 @@ 'use server' import { revalidatePath } from 'next/cache' -import { cookies } from 'next/headers' -import { COOKIE_KEYS } from '@/configs/cookies' import { createWebhooksRepository } from '@/core/modules/webhooks/repository.server' import { authActionClient, withTeamAuthedRequestRepository, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { handleDefaultInfraError } from '@/core/server/actions/utils' import { @@ -30,11 +28,19 @@ const withWebhooksRepository = withTeamAuthedRequestRepository( export const upsertWebhookAction = authActionClient .schema(UpsertWebhookSchema) .metadata({ actionName: 'upsertWebhook' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { - const { mode, webhookId, name, url, events, signatureSecret, enabled } = - parsedInput + const { + mode, + teamSlug, + webhookId, + name, + url, + events, + signatureSecret, + enabled, + } = parsedInput const { session, teamId } = ctx const response = await ctx.webhooksRepository.upsertWebhook({ @@ -74,10 +80,6 @@ export const upsertWebhookAction = authActionClient return handleDefaultInfraError(status) } - const teamSlug = (await cookies()).get( - COOKIE_KEYS.SELECTED_TEAM_SLUG - )?.value - revalidatePath(`/dashboard/${teamSlug}/webhooks`, 'page') return { success: true } @@ -88,10 +90,10 @@ export const upsertWebhookAction = authActionClient export const deleteWebhookAction = authActionClient .schema(DeleteWebhookSchema) .metadata({ actionName: 'deleteWebhook' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { - const { webhookId } = parsedInput + const { teamSlug, webhookId } = parsedInput const { session, teamId } = ctx const response = await ctx.webhooksRepository.deleteWebhook(webhookId) @@ -116,10 +118,6 @@ export const deleteWebhookAction = authActionClient return handleDefaultInfraError(status) } - const teamSlug = (await cookies()).get( - COOKIE_KEYS.SELECTED_TEAM_SLUG - )?.value - revalidatePath(`/dashboard/${teamSlug}/webhooks`, 'page') return { success: true } @@ -130,10 +128,10 @@ export const deleteWebhookAction = authActionClient export const updateWebhookSecretAction = authActionClient .schema(UpdateWebhookSecretSchema) .metadata({ actionName: 'updateWebhookSecret' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withWebhooksRepository) .action(async ({ parsedInput, ctx }) => { - const { webhookId, signatureSecret } = parsedInput + const { teamSlug, webhookId, signatureSecret } = parsedInput const { session, teamId } = ctx const response = await ctx.webhooksRepository.updateWebhookSecret( @@ -162,10 +160,6 @@ export const updateWebhookSecretAction = authActionClient return handleDefaultInfraError(status) } - const teamSlug = (await cookies()).get( - COOKIE_KEYS.SELECTED_TEAM_SLUG - )?.value - revalidatePath(`/dashboard/${teamSlug}/webhooks`, 'page') return { success: true } diff --git a/src/core/server/functions/keys/get-api-keys.ts b/src/core/server/functions/keys/get-api-keys.ts index acd6ab857..208af3d34 100644 --- a/src/core/server/functions/keys/get-api-keys.ts +++ b/src/core/server/functions/keys/get-api-keys.ts @@ -4,26 +4,25 @@ import { CACHE_TAGS } from '@/configs/cache' import { createKeysRepository } from '@/core/modules/keys/repository.server' import { authActionClient, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { handleDefaultInfraError } from '@/core/server/actions/utils' import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const GetApiKeysSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) export const getTeamApiKeys = authActionClient .schema(GetApiKeysSchema) .metadata({ serverFunctionName: 'getTeamApiKeys' }) - .use(withTeamIdResolution) - .action(async ({ ctx, parsedInput }) => { + .use(withTeamSlugResolution) + .action(async ({ ctx }) => { 'use cache' cacheLife('default') - cacheTag(CACHE_TAGS.TEAM_API_KEYS(parsedInput.teamIdOrSlug)) - const { session, teamId } = ctx + cacheTag(CACHE_TAGS.TEAM_API_KEYS(teamId)) const result = await createKeysRepository({ accessToken: session.access_token, diff --git a/src/core/server/functions/sandboxes/get-team-metrics-max.ts b/src/core/server/functions/sandboxes/get-team-metrics-max.ts index 3fee161f8..779ccce32 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics-max.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics-max.ts @@ -6,17 +6,17 @@ import { USE_MOCK_DATA } from '@/configs/flags' import { MOCK_TEAM_METRICS_MAX_DATA } from '@/configs/mock-data' import { authActionClient, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { handleDefaultInfraError } from '@/core/server/actions/utils' import { infra } from '@/core/shared/clients/api' import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' export const GetTeamMetricsMaxSchema = z .object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, startDate: z .number() .int() @@ -54,7 +54,7 @@ export const GetTeamMetricsMaxSchema = z export const getTeamMetricsMax = authActionClient .metadata({ serverFunctionName: 'getTeamMetricsMax' }) .schema(GetTeamMetricsMaxSchema) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .action(async ({ parsedInput, ctx }) => { const { session, teamId } = ctx const { startDate: startDateMs, endDate: endDateMs, metric } = parsedInput diff --git a/src/core/server/functions/sandboxes/get-team-metrics.ts b/src/core/server/functions/sandboxes/get-team-metrics.ts index 51d066605..ece07f138 100644 --- a/src/core/server/functions/sandboxes/get-team-metrics.ts +++ b/src/core/server/functions/sandboxes/get-team-metrics.ts @@ -3,16 +3,16 @@ import 'server-only' import { z } from 'zod' import { authActionClient, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { returnServerError } from '@/core/server/actions/utils' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' import { MAX_DAYS_AGO } from '@/features/dashboard/sandboxes/monitoring/time-picker/constants' import { getTeamMetricsCore } from './get-team-metrics-core' export const GetTeamMetricsSchema = z .object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, startDate: z .number() .int() @@ -49,7 +49,7 @@ export const GetTeamMetricsSchema = z export const getTeamMetrics = authActionClient .schema(GetTeamMetricsSchema) .metadata({ serverFunctionName: 'getTeamMetrics' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .action(async ({ parsedInput, ctx }) => { const { session, teamId } = ctx diff --git a/src/core/server/functions/team/get-team-id-from-segment.ts b/src/core/server/functions/team/get-team-id-from-segment.ts deleted file mode 100644 index f943654a4..000000000 --- a/src/core/server/functions/team/get-team-id-from-segment.ts +++ /dev/null @@ -1,48 +0,0 @@ -import 'server-only' - -import z from 'zod' -import { CACHE_TAGS } from '@/configs/cache' -import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' -import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' - -export const getTeamIdFromSegment = async ( - segment: string, - accessToken: string -) => { - if (!TeamIdOrSlugSchema.safeParse(segment).success) { - l.warn( - { - key: 'get_team_id_from_segment:invalid_segment', - context: { segment }, - }, - 'get_team_id_from_segment - invalid segment' - ) - - return null - } - - if (z.uuid().safeParse(segment).success) { - return segment - } - - const resolvedTeam = await createUserTeamsRepository({ - accessToken, - }).resolveTeamBySlug(segment, { - tags: [CACHE_TAGS.TEAM_ID_FROM_SEGMENT(segment)], - }) - - if (!resolvedTeam.ok) { - l.warn( - { - key: 'get_team_id_from_segment:resolve_failed', - context: { segment }, - }, - 'get_team_id_from_segment - failed to resolve' - ) - - return null - } - - return resolvedTeam.data.id -} diff --git a/src/core/server/functions/team/get-team-id-from-slug.ts b/src/core/server/functions/team/get-team-id-from-slug.ts new file mode 100644 index 000000000..1ce9aa68e --- /dev/null +++ b/src/core/server/functions/team/get-team-id-from-slug.ts @@ -0,0 +1,43 @@ +import 'server-only' + +import { CACHE_TAGS } from '@/configs/cache' +import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server' +import { l } from '@/core/shared/clients/logger/logger' +import { TeamSlugSchema } from '@/core/shared/schemas/team' + +export const getTeamIdFromSlug = async ( + teamSlug: string, + accessToken: string +) => { + if (!TeamSlugSchema.safeParse(teamSlug).success) { + l.warn( + { + key: 'get_team_id_from_slug:invalid_team_slug', + context: { teamSlug }, + }, + 'get_team_id_from_slug - invalid team slug' + ) + + return null + } + + const resolvedTeam = await createUserTeamsRepository({ + accessToken, + }).resolveTeamBySlug(teamSlug, { + tags: [CACHE_TAGS.TEAM_ID_FROM_SLUG(teamSlug)], + }) + + if (!resolvedTeam.ok) { + l.warn( + { + key: 'get_team_id_from_slug:resolve_failed', + context: { teamSlug }, + }, + 'get_team_id_from_slug - failed to resolve' + ) + + return null + } + + return resolvedTeam.data.id +} diff --git a/src/core/server/functions/team/get-team-members.ts b/src/core/server/functions/team/get-team-members.ts index e77fce38e..7eb4182e8 100644 --- a/src/core/server/functions/team/get-team-members.ts +++ b/src/core/server/functions/team/get-team-members.ts @@ -5,10 +5,10 @@ import { createTeamsRepository } from '@/core/modules/teams/teams-repository.ser import { authActionClient, withTeamAuthedRequestRepository, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { toActionErrorFromRepoError } from '@/core/server/adapters/repo-error' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const withTeamsRepository = withTeamAuthedRequestRepository( createTeamsRepository, @@ -16,13 +16,13 @@ const withTeamsRepository = withTeamAuthedRequestRepository( ) const GetTeamMembersSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) export const getTeamMembers = authActionClient .schema(GetTeamMembersSchema) .metadata({ serverFunctionName: 'getTeamMembers' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withTeamsRepository) .action(async ({ ctx }) => { const result = await ctx.teamsRepository.listTeamMembers() diff --git a/src/core/server/functions/team/resolve-user-team.ts b/src/core/server/functions/team/resolve-user-team.ts index 5f061fd4a..a577348f0 100644 --- a/src/core/server/functions/team/resolve-user-team.ts +++ b/src/core/server/functions/team/resolve-user-team.ts @@ -36,13 +36,27 @@ export async function resolveUserTeam( return null } - const defaultTeam = teamsResult.data.find((t) => t.isDefault) - const team = defaultTeam ?? teamsResult.data[0] + const defaultTeam = teamsResult.data.find( + (team) => team.isDefault && team.slug + ) + const team = + defaultTeam ?? teamsResult.data.find((candidate) => candidate.slug) if (!team) { return null } + if (!team.slug) { + l.warn( + { + key: 'resolve_user_team:missing_team_slug', + team_id: team.id, + }, + 'Failed to resolve a slug-backed team' + ) + return null + } + return { id: team.id, slug: team.slug, diff --git a/src/core/server/functions/usage/get-usage.ts b/src/core/server/functions/usage/get-usage.ts index 7377e0faa..f63969f3a 100644 --- a/src/core/server/functions/usage/get-usage.ts +++ b/src/core/server/functions/usage/get-usage.ts @@ -6,19 +6,19 @@ import { CACHE_TAGS } from '@/configs/cache' import { createBillingRepository } from '@/core/modules/billing/repository.server' import { authActionClient, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { returnServerError } from '@/core/server/actions/utils' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const GetUsageAuthActionSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) export const getUsage = authActionClient .schema(GetUsageAuthActionSchema) .metadata({ serverFunctionName: 'getUsage' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .action(async ({ ctx }) => { 'use cache' diff --git a/src/core/server/functions/webhooks/get-webhooks.ts b/src/core/server/functions/webhooks/get-webhooks.ts index 78df2d835..4e25493dd 100644 --- a/src/core/server/functions/webhooks/get-webhooks.ts +++ b/src/core/server/functions/webhooks/get-webhooks.ts @@ -5,14 +5,14 @@ import { createWebhooksRepository } from '@/core/modules/webhooks/repository.ser import { authActionClient, withTeamAuthedRequestRepository, - withTeamIdResolution, + withTeamSlugResolution, } from '@/core/server/actions/client' import { handleDefaultInfraError } from '@/core/server/actions/utils' import { l } from '@/core/shared/clients/logger/logger' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const GetWebhooksSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) const withWebhooksRepository = withTeamAuthedRequestRepository( @@ -23,7 +23,7 @@ const withWebhooksRepository = withTeamAuthedRequestRepository( export const getWebhooks = authActionClient .schema(GetWebhooksSchema) .metadata({ serverFunctionName: 'getWebhook' }) - .use(withTeamIdResolution) + .use(withTeamSlugResolution) .use(withWebhooksRepository) .action(async ({ ctx }) => { const { session, teamId } = ctx diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index bfcfe13e7..5d61057d1 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' const WebhookUrlSchema = z.httpUrl('Must be a valid URL').trim() const WebhookSecretSchema = z @@ -9,7 +9,7 @@ const WebhookSecretSchema = z export const UpsertWebhookSchema = z .object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, mode: z.enum(['add', 'edit']), webhookId: z.uuid().optional(), name: z.string().min(1, 'Name is required').trim(), @@ -33,12 +33,12 @@ export const UpsertWebhookSchema = z ) export const DeleteWebhookSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, webhookId: z.uuid(), }) export const UpdateWebhookSecretSchema = z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, webhookId: z.uuid(), signatureSecret: WebhookSecretSchema, }) diff --git a/src/core/server/trpc/procedures.ts b/src/core/server/trpc/procedures.ts index 4fc5950b2..c62e23194 100644 --- a/src/core/server/trpc/procedures.ts +++ b/src/core/server/trpc/procedures.ts @@ -6,9 +6,9 @@ import { endTelemetryMiddleware, startTelemetryMiddleware, } from '@/core/server/api/middlewares/telemetry' -import { getTeamIdFromSegment } from '@/core/server/functions/team/get-team-id-from-segment' +import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug' import { getTracer } from '@/core/shared/clients/tracer' -import { TeamIdOrSlugSchema } from '@/core/shared/schemas/team' +import { TeamSlugSchema } from '@/core/shared/schemas/team' import { t } from './init' /** @@ -55,7 +55,7 @@ export const protectedProcedure = t.procedure /** * Protected Team Procedure * - * Used to create protected routes that require authentication and a team authorization, via teamIdOrSlug. + * Used to create protected routes that require authentication and a team authorization, via teamSlug. * */ export const protectedTeamProcedure = t.procedure @@ -63,7 +63,7 @@ export const protectedTeamProcedure = t.procedure .use(authMiddleware) .input( z.object({ - teamIdOrSlug: TeamIdOrSlugSchema, + teamSlug: TeamSlugSchema, }) ) .use(async ({ ctx, next, input }) => { @@ -75,8 +75,8 @@ export const protectedTeamProcedure = t.procedure const teamId = await context.with( trace.setSpan(context.active(), span), async () => { - return await getTeamIdFromSegment( - input.teamIdOrSlug, + return await getTeamIdFromSlug( + input.teamSlug, ctx.session.access_token ) } @@ -85,7 +85,7 @@ export const protectedTeamProcedure = t.procedure if (!teamId) { span.setStatus({ code: SpanStatusCode.ERROR, - message: `teamId not found for teamIdOrSlug (${input.teamIdOrSlug})`, + message: `teamId not found for teamSlug (${input.teamSlug})`, }) throw forbiddenTeamAccessError() diff --git a/src/core/shared/schemas/team.ts b/src/core/shared/schemas/team.ts index 66c312573..45a4beb53 100644 --- a/src/core/shared/schemas/team.ts +++ b/src/core/shared/schemas/team.ts @@ -1,3 +1,3 @@ import { z } from 'zod' -export const TeamIdOrSlugSchema = z.union([z.uuid(), z.string()]) +export const TeamSlugSchema = z.string().trim().min(1) diff --git a/src/features/dashboard/account/password-settings-server.tsx b/src/features/dashboard/account/password-settings-server.tsx index ea74e0227..a92c9e2f8 100644 --- a/src/features/dashboard/account/password-settings-server.tsx +++ b/src/features/dashboard/account/password-settings-server.tsx @@ -1,4 +1,4 @@ -import type { AccountPageSearchParams } from '@/app/dashboard/[teamIdOrSlug]/account/page' +import type { AccountPageSearchParams } from '@/app/dashboard/[teamSlug]/account/page' import { PasswordSettings } from './password-settings' interface PasswordSettingsServerProps { diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index 8557780a1..eb12e5f6c 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -164,8 +164,7 @@ function AddonsLoading() { } function AddonsUpgradePlaceholder() { - const { teamIdOrSlug } = - useRouteParams<'/dashboard/[teamIdOrSlug]/billing/plan'>() + const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/billing/plan'>() return (
@@ -175,7 +174,7 @@ function AddonsUpgradePlaceholder() { Upgrade to Pro to purchase add-ons for higher concurrency limits.

@@ -131,14 +131,14 @@ function PlanDetails({ <> {isBaseTier ? ( ) : ( diff --git a/src/features/dashboard/build/build-logs-store.ts b/src/features/dashboard/build/build-logs-store.ts index 26b9de7f3..9d5b72d10 100644 --- a/src/features/dashboard/build/build-logs-store.ts +++ b/src/features/dashboard/build/build-logs-store.ts @@ -15,7 +15,7 @@ import type { LogLevelFilter } from './logs-filter-params' const EMPTY_INIT_FORWARD_LOOKBACK_MS = 5_000 interface BuildLogsParams { - teamIdOrSlug: string + teamSlug: string templateId: string buildId: string } @@ -95,7 +95,7 @@ export const createBuildLogsStore = () => const paramsChanged = state._params?.buildId !== params.buildId || state._params?.templateId !== params.templateId || - state._params?.teamIdOrSlug !== params.teamIdOrSlug + state._params?.teamSlug !== params.teamSlug const levelChanged = state.level !== level if (paramsChanged || levelChanged || !state.isInitialized) { @@ -118,7 +118,7 @@ export const createBuildLogsStore = () => const result = await trpcClient.builds.buildLogsBackwardsReversed.query({ - teamIdOrSlug: params.teamIdOrSlug, + teamSlug: params.teamSlug, templateId: params.templateId, buildId: params.buildId, level: level ?? undefined, @@ -193,7 +193,7 @@ export const createBuildLogsStore = () => const result = await state._trpcClient.builds.buildLogsBackwardsReversed.query({ - teamIdOrSlug: state._params.teamIdOrSlug, + teamSlug: state._params.teamSlug, templateId: state._params.templateId, buildId: state._params.buildId, level: state.level ?? undefined, @@ -253,7 +253,7 @@ export const createBuildLogsStore = () => const seenAtCursor = state.forwardSeenAtCursor const result = await state._trpcClient.builds.buildLogsForward.query({ - teamIdOrSlug: state._params.teamIdOrSlug, + teamSlug: state._params.teamSlug, templateId: state._params.templateId, buildId: state._params.buildId, level: state.level ?? undefined, diff --git a/src/features/dashboard/build/header-cells.tsx b/src/features/dashboard/build/header-cells.tsx index e4628638f..ede75eb91 100644 --- a/src/features/dashboard/build/header-cells.tsx +++ b/src/features/dashboard/build/header-cells.tsx @@ -23,8 +23,7 @@ export function Template({ className?: string }) { const router = useRouter() - const { teamIdOrSlug } = - useRouteParams<'/dashboard/[teamIdOrSlug]/templates'>() + const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/templates'>() return (