From e70270fc46bd13c2645d2fa8f689513b9c6f07d9 Mon Sep 17 00:00:00 2001 From: Francesco Carrella Date: Fri, 26 Jun 2026 10:10:29 +0200 Subject: [PATCH 1/2] refactor: depend on @canup/contracts (renamed from @canup/types) The shared wire-contract package was renamed. Update the dependency, all import specifiers, and the lockfile to match. Mechanical; no behavior change. --- packages/cli/package.json | 2 +- packages/cli/src/api-client.ts | 4 ++-- packages/cli/src/commands/actions/_shared.ts | 2 +- packages/cli/src/commands/deps/_shared.ts | 2 +- packages/ui/package.json | 2 +- packages/ui/src/hooks/use-action.test.ts | 2 +- packages/ui/src/hooks/use-credits.test.ts | 2 +- packages/ui/src/hooks/use-credits.ts | 2 +- packages/ui/src/hooks/use-customer.test.ts | 2 +- packages/ui/src/hooks/use-customer.ts | 2 +- packages/ui/src/index.ts | 2 +- packages/ui/src/internal/api-client.ts | 2 +- packages/ui/src/internal/realtime.ts | 2 +- packages/ui/tsdown.config.ts | 2 +- pnpm-lock.yaml | 10 +++++----- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index b5c6c70..5b97882 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,7 +42,7 @@ "ora": "^9.3.0" }, "devDependencies": { - "@canup/types": "^0.2.0", + "@canup/contracts": "^0.2.0", "@types/node": "^22.13.4", "type-fest": "^5.5.0" } diff --git a/packages/cli/src/api-client.ts b/packages/cli/src/api-client.ts index eec895e..2bff7b8 100644 --- a/packages/cli/src/api-client.ts +++ b/packages/cli/src/api-client.ts @@ -1,7 +1,7 @@ /** * HTTP client for the CanUp API. * - * Uses native fetch (Node.js 18+). Wire shapes come from `@canup/types`, the + * Uses native fetch (Node.js 18+). Wire shapes come from `@canup/contracts`, the * published contract every endpoint serializes — the client casts responses to * those types rather than re-declaring them. All app-scoped methods take an * appId and build app-scoped URLs. The API returns camelCase natively, so there @@ -35,7 +35,7 @@ import type { StripeStatusResult, TestResult, User, -} from '@canup/types'; +} from '@canup/contracts'; import { DEFAULT_API_URL, API_VERSION } from './constants.js'; import { CLI_USER_AGENT } from './version.js'; import { ApiError } from './errors.js'; diff --git a/packages/cli/src/commands/actions/_shared.ts b/packages/cli/src/commands/actions/_shared.ts index 259f619..b7c99d6 100644 --- a/packages/cli/src/commands/actions/_shared.ts +++ b/packages/cli/src/commands/actions/_shared.ts @@ -1,6 +1,6 @@ import { readFileSync, existsSync } from 'node:fs'; import { resolve } from 'node:path'; -import type { TestResult } from '@canup/types'; +import type { TestResult } from '@canup/contracts'; import { success, error, dim } from '../../ui/output.js'; import { formatDuration } from '../../lib/format.js'; diff --git a/packages/cli/src/commands/deps/_shared.ts b/packages/cli/src/commands/deps/_shared.ts index f1c1b19..4f7a6ce 100644 --- a/packages/cli/src/commands/deps/_shared.ts +++ b/packages/cli/src/commands/deps/_shared.ts @@ -1,4 +1,4 @@ -import type { Language, PackageSpec } from '@canup/types'; +import type { Language, PackageSpec } from '@canup/contracts'; import { type CanupClient } from '../../api-client.js'; import { error } from '../../ui/output.js'; import { createSpinner } from '../../ui/spinner.js'; diff --git a/packages/ui/package.json b/packages/ui/package.json index b1e4982..b91fd1e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -45,7 +45,7 @@ "eventsource": "^4.1.0" }, "devDependencies": { - "@canup/types": "^0.2.0", + "@canup/contracts": "^0.2.0", "@canva/app-i18n-kit": "^1.2.0", "@canva/app-ui-kit": "^5.9.0", "@canva/platform": "^2.2.2", diff --git a/packages/ui/src/hooks/use-action.test.ts b/packages/ui/src/hooks/use-action.test.ts index f937e42..feedd57 100644 --- a/packages/ui/src/hooks/use-action.test.ts +++ b/packages/ui/src/hooks/use-action.test.ts @@ -4,7 +4,7 @@ import { useAction } from './use-action.js'; import { runAction } from '../internal/api-client.js'; import { queryClient, creditKey } from '../internal/query.js'; import { CanupError } from '../errors.js'; -import type { CreditBalance } from '@canup/types'; +import type { CreditBalance } from '@canup/contracts'; vi.mock('../internal/api-client.js', () => ({ runAction: vi.fn(), diff --git a/packages/ui/src/hooks/use-credits.test.ts b/packages/ui/src/hooks/use-credits.test.ts index 3bcd470..bd09c5a 100644 --- a/packages/ui/src/hooks/use-credits.test.ts +++ b/packages/ui/src/hooks/use-credits.test.ts @@ -4,7 +4,7 @@ import { useCredits } from './use-credits.js'; import { fetchCredits } from '../internal/api-client.js'; import { queryClient, creditKey } from '../internal/query.js'; import { acquire } from '../internal/realtime.js'; -import type { CreditBalance } from '@canup/types'; +import type { CreditBalance } from '@canup/contracts'; vi.mock('../internal/api-client.js', () => ({ fetchCredits: vi.fn(), diff --git a/packages/ui/src/hooks/use-credits.ts b/packages/ui/src/hooks/use-credits.ts index 6423bf3..afb2202 100644 --- a/packages/ui/src/hooks/use-credits.ts +++ b/packages/ui/src/hooks/use-credits.ts @@ -4,7 +4,7 @@ import { queryClient, creditKey } from '../internal/query.js'; import { fetchCredits } from '../internal/api-client.js'; import { acquire } from '../internal/realtime.js'; import { type CanupError, toCanupError } from '../errors.js'; -import type { CreditBalance } from '@canup/types'; +import type { CreditBalance } from '@canup/contracts'; export interface UseCreditsResult { data: CreditBalance | null; diff --git a/packages/ui/src/hooks/use-customer.test.ts b/packages/ui/src/hooks/use-customer.test.ts index 52f90eb..1959fec 100644 --- a/packages/ui/src/hooks/use-customer.test.ts +++ b/packages/ui/src/hooks/use-customer.test.ts @@ -4,7 +4,7 @@ import { useCustomer } from './use-customer.js'; import { fetchCustomer } from '../internal/api-client.js'; import { queryClient, customerKey } from '../internal/query.js'; import { acquire } from '../internal/realtime.js'; -import type { Customer } from '@canup/types'; +import type { Customer } from '@canup/contracts'; vi.mock('../internal/api-client.js', () => ({ fetchCustomer: vi.fn(), diff --git a/packages/ui/src/hooks/use-customer.ts b/packages/ui/src/hooks/use-customer.ts index 8409851..c685297 100644 --- a/packages/ui/src/hooks/use-customer.ts +++ b/packages/ui/src/hooks/use-customer.ts @@ -4,7 +4,7 @@ import { queryClient, customerKey } from '../internal/query.js'; import { fetchCustomer } from '../internal/api-client.js'; import { acquire } from '../internal/realtime.js'; import { type CanupError, toCanupError } from '../errors.js'; -import type { Customer, SubscriptionStatus } from '@canup/types'; +import type { Customer, SubscriptionStatus } from '@canup/contracts'; export interface UseCustomerResult { /** Canonical app name, for attributing every credit/plan/manage noun. */ diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6ade015..6c5cf4f 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -17,5 +17,5 @@ export type { UseCustomerResult } from './hooks/use-customer.js'; // The `SubscriptionStatus` enum is intentionally not re-exported under that name // — it would collide with the `` component. Consumers who // need the union can reach it via `Customer['subscriptionStatus']`. -export type { CreditBalance, Customer } from '@canup/types'; +export type { CreditBalance, Customer } from '@canup/contracts'; export { CanupError } from './errors.js'; diff --git a/packages/ui/src/internal/api-client.ts b/packages/ui/src/internal/api-client.ts index 52c8c10..54bd673 100644 --- a/packages/ui/src/internal/api-client.ts +++ b/packages/ui/src/internal/api-client.ts @@ -6,7 +6,7 @@ import type { Customer, RunResult, SubscribeLinkResult, -} from '@canup/types'; +} from '@canup/contracts'; declare global { var __canup_url: string | undefined; diff --git a/packages/ui/src/internal/realtime.ts b/packages/ui/src/internal/realtime.ts index 7b5d9dc..aa90a93 100644 --- a/packages/ui/src/internal/realtime.ts +++ b/packages/ui/src/internal/realtime.ts @@ -1,5 +1,5 @@ import { EventSource } from 'eventsource'; -import type { RealtimeEvent } from '@canup/types'; +import type { RealtimeEvent } from '@canup/contracts'; import { getJwt } from './jwt-cache.js'; import { getBaseUrl } from './api-client.js'; import { queryClient, creditKey, customerKey } from './query.js'; diff --git a/packages/ui/tsdown.config.ts b/packages/ui/tsdown.config.ts index 17077d4..b79c94a 100644 --- a/packages/ui/tsdown.config.ts +++ b/packages/ui/tsdown.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ entry: { index: 'src/index.ts' }, outDir: 'dist', format: 'esm', - // eager: full tsc-driven declarations so the re-exported @canup/types types + // eager: full tsc-driven declarations so the re-exported @canup/contracts types // (e.g. CreditBalance) inline cleanly into the bundled .d.ts. dts: { eager: true }, platform: 'browser', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fcefc5..ce5ac07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,7 +151,7 @@ importers: specifier: ^9.3.0 version: 9.3.0 devDependencies: - '@canup/types': + '@canup/contracts': specifier: ^0.2.0 version: 0.2.0 '@types/node': @@ -170,7 +170,7 @@ importers: specifier: ^4.1.0 version: 4.1.0 devDependencies: - '@canup/types': + '@canup/contracts': specifier: ^0.2.0 version: 0.2.0 '@canva/app-i18n-kit': @@ -286,8 +286,8 @@ packages: '@braidai/lang@1.1.2': resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} - '@canup/types@0.2.0': - resolution: {integrity: sha512-RT1luUfZ730VpO0uODJs5EfHiGl7uxZQn/pPB75kVHZ9dbCHR1v+ivmT5wylg+Y2X14LYzQRCvU4lskCnK5HMQ==} + '@canup/contracts@0.2.0': + resolution: {integrity: sha512-fWzARwXMDYbDKTr+oxsqdZdhEwBE/9IBTUnm7/N/7AQwK0yBF2w5Fn/F2yOMlbhAcsfZUIt9UL5EjoYcFmXfJw==} '@canva/app-hooks@0.0.0-beta.4': resolution: {integrity: sha512-QPIoVyIRz55Ta0PNxD07OHvyGOD7R1irwEPfs1T7At21TJHFAnlTt/Je8IfZcm6lrNR6oOSBBZrRiFeJA49PEg==} @@ -3927,7 +3927,7 @@ snapshots: '@braidai/lang@1.1.2': {} - '@canup/types@0.2.0': {} + '@canup/contracts@0.2.0': {} '@canva/app-hooks@0.0.0-beta.4(@canva/design@2.8.0(@canva/error@2.2.1))(@canva/platform@2.2.2(@canva/error@2.2.1))(react@19.2.4)': dependencies: From b32694ba1e444f5dc5bc5ba7b9e147faea79578b Mon Sep 17 00:00:00 2001 From: Francesco Carrella Date: Fri, 26 Jun 2026 10:23:08 +0200 Subject: [PATCH 2/2] refactor: adopt the shared error kernel from @canup/contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both packages shipped their own error type — the UI a local CanupError (emitting NETWORK_ERROR, carrying a details bag) and the CLI an ApiError (field statusCode) — duplicating, and diverging from, the canonical kernel that @canup/contracts already exports. Delete both local classes and adopt the shared CanupError, unwrapResponse, toCanupError, and isCanupError. Behavior changes, with the contract as the source of truth: - Error codes are now the contract's vocabulary. The client-transport failure code is TRANSPORT_ERROR (was NETWORK_ERROR in the UI and HttpError in the CLI); neither old code exists in the contract. Server codes still pass through verbatim via the open union. - The HTTP status field is httpStatus (was the CLI's statusCode). - Response decoding goes through the contract's unwrapResponse, which also guards a non-JSON body — previously unguarded in the CLI client. Tests updated to assert the contract vocabulary (TRANSPORT_ERROR, httpStatus); they encode the adopted contract, not a behavior tweak. The kernel is bundled into each package's dist, so consumers gain no runtime dependency. --- packages/cli/src/api-client.test.ts | 32 +++++++++---------- packages/cli/src/api-client.ts | 17 +++------- .../cli/src/commands/actions/delete.test.ts | 8 ++--- packages/cli/src/commands/actions/delete.ts | 4 +-- .../cli/src/commands/actions/deploy.test.ts | 8 ++--- packages/cli/src/commands/actions/deploy.ts | 4 +-- .../src/commands/actions/invocations.test.ts | 8 ++--- .../cli/src/commands/actions/invocations.ts | 6 ++-- .../cli/src/commands/actions/remove.test.ts | 8 ++--- packages/cli/src/commands/actions/remove.ts | 4 +-- packages/cli/src/commands/actions/run.test.ts | 8 ++--- packages/cli/src/commands/actions/run.ts | 6 ++-- .../cli/src/commands/actions/test.test.ts | 8 ++--- packages/cli/src/commands/actions/test.ts | 6 ++-- packages/cli/src/commands/deps/add.test.ts | 4 +-- packages/cli/src/commands/deps/add.ts | 4 +-- packages/cli/src/commands/deps/clear.test.ts | 8 ++--- packages/cli/src/commands/deps/clear.ts | 4 +-- packages/cli/src/commands/deps/remove.test.ts | 8 ++--- packages/cli/src/commands/deps/remove.ts | 6 ++-- packages/cli/src/commands/init.test.ts | 16 +++++----- packages/cli/src/commands/init.ts | 6 ++-- packages/cli/src/commands/pull.test.ts | 4 +-- packages/cli/src/commands/pull.ts | 4 +-- .../cli/src/commands/secrets/delete.test.ts | 4 +-- packages/cli/src/commands/secrets/delete.ts | 4 +-- packages/cli/src/commands/status.test.ts | 4 +-- packages/cli/src/commands/status.ts | 4 +-- .../cli/src/commands/stripe/connect.test.ts | 6 ++-- packages/cli/src/commands/stripe/connect.ts | 6 ++-- packages/cli/src/commands/whoami.test.ts | 4 +-- packages/cli/src/commands/whoami.ts | 4 +-- packages/cli/src/errors.ts | 12 ------- .../ui/src/components/ActionButton.test.tsx | 2 +- packages/ui/src/components/ActionButton.tsx | 2 +- packages/ui/src/errors.ts | 17 ---------- packages/ui/src/hooks/use-action.test.ts | 6 ++-- packages/ui/src/hooks/use-action.ts | 2 +- packages/ui/src/hooks/use-credits.test.ts | 2 +- packages/ui/src/hooks/use-credits.ts | 2 +- packages/ui/src/hooks/use-customer.test.ts | 2 +- packages/ui/src/hooks/use-customer.ts | 2 +- packages/ui/src/index.ts | 2 +- packages/ui/src/internal/api-client.test.ts | 2 +- packages/ui/src/internal/api-client.ts | 25 ++------------- 45 files changed, 125 insertions(+), 180 deletions(-) delete mode 100644 packages/cli/src/errors.ts delete mode 100644 packages/ui/src/errors.ts diff --git a/packages/cli/src/api-client.test.ts b/packages/cli/src/api-client.test.ts index 8cd777d..bbfb8a1 100644 --- a/packages/cli/src/api-client.test.ts +++ b/packages/cli/src/api-client.test.ts @@ -1,6 +1,6 @@ import { describe, test as baseTest, expect, vi } from 'vitest'; import { CanupClient } from './api-client.js'; -import { ApiError } from './errors.js'; +import { CanupError } from '@canup/contracts'; // ────────────────────────────────────────────── // Fetch mock setup @@ -154,16 +154,16 @@ describe('CanupClient', () => { // ──── Error handling (via request()) ──── describe('error handling via request()', () => { - test('throws an ApiError with statusCode and code on API error envelope', async () => { + test('throws a CanupError with httpStatus and code on API error envelope', async () => { mockFetch.mockResolvedValueOnce(errorResponse('NotFoundError', 'App not found', 404)); const client = createClient(); const err = await client.getMe().catch((e: unknown) => e); - expect(err).toBeInstanceOf(ApiError); - expect((err as ApiError).message).toBe('App not found'); - expect((err as ApiError).statusCode).toBe(404); - expect((err as ApiError).code).toBe('NotFoundError'); + expect(err).toBeInstanceOf(CanupError); + expect((err as CanupError).message).toBe('App not found'); + expect((err as CanupError).httpStatus).toBe(404); + expect((err as CanupError).code).toBe('NotFoundError'); }); test('error message matches the API error message', async () => { @@ -473,7 +473,7 @@ describe('CanupClient', () => { expect(result).toHaveProperty('ok', false); }); - test('throws an ApiError on HTTP error with statusCode and code', async () => { + test('throws a CanupError on HTTP error with httpStatus and code', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, @@ -484,13 +484,13 @@ describe('CanupClient', () => { const err = await client.testCode('a1', 'code', 'nodejs', {}).catch((e: unknown) => e); - expect(err).toBeInstanceOf(ApiError); - expect((err as ApiError).message).toBe('Invalid token'); - expect((err as ApiError).statusCode).toBe(401); - expect((err as ApiError).code).toBe('AuthError'); + expect(err).toBeInstanceOf(CanupError); + expect((err as CanupError).message).toBe('Invalid token'); + expect((err as CanupError).httpStatus).toBe(401); + expect((err as CanupError).code).toBe('AuthError'); }); - test('falls back to statusText and HttpError when response is not JSON', async () => { + test('falls back to statusText and TRANSPORT_ERROR when response is not JSON', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, @@ -501,10 +501,10 @@ describe('CanupClient', () => { const err = await client.testCode('a1', 'code', 'nodejs', {}).catch((e: unknown) => e); - expect(err).toBeInstanceOf(ApiError); - expect((err as ApiError).message).toBe('Internal Server Error'); - expect((err as ApiError).statusCode).toBe(500); - expect((err as ApiError).code).toBe('HttpError'); + expect(err).toBeInstanceOf(CanupError); + expect((err as CanupError).message).toBe('Internal Server Error'); + expect((err as CanupError).httpStatus).toBe(500); + expect((err as CanupError).code).toBe('TRANSPORT_ERROR'); }); test('sends Authorization header when token is set', async () => { diff --git a/packages/cli/src/api-client.ts b/packages/cli/src/api-client.ts index 2bff7b8..f41ff5f 100644 --- a/packages/cli/src/api-client.ts +++ b/packages/cli/src/api-client.ts @@ -11,7 +11,6 @@ import type { Action, ApiKeyCreateResult, - ApiResponse, App, AppListItem, Build, @@ -38,7 +37,7 @@ import type { } from '@canup/contracts'; import { DEFAULT_API_URL, API_VERSION } from './constants.js'; import { CLI_USER_AGENT } from './version.js'; -import { ApiError } from './errors.js'; +import { CanupError, unwrapResponse } from '@canup/contracts'; export class CanupClient { private apiUrl: string; @@ -205,10 +204,10 @@ export class CanupClient { } catch { // non-JSON error response } - throw new ApiError( - res.status, - body?.error?.code ?? 'HttpError', + throw new CanupError( + body?.error?.code ?? 'TRANSPORT_ERROR', body?.error?.message ?? res.statusText, + res.status, ); } @@ -414,12 +413,6 @@ export class CanupClient { signal: options?.signal, }); - const body = (await res.json()) as ApiResponse; - - if (!body.ok) { - throw new ApiError(res.status, body.error.code, body.error.message); - } - - return body.data; + return unwrapResponse(res); } } diff --git a/packages/cli/src/commands/actions/delete.test.ts b/packages/cli/src/commands/actions/delete.test.ts index 3dfec1a..11d5cb0 100644 --- a/packages/cli/src/commands/actions/delete.test.ts +++ b/packages/cli/src/commands/actions/delete.test.ts @@ -32,8 +32,8 @@ describe('actions delete command', () => { }); test('handles 404 error when action not found', async ({ client, output, processMocks }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.deleteAction.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -50,8 +50,8 @@ describe('actions delete command', () => { }); test('handles generic error', async ({ client, output, processMocks }) => { - const apiError = new Error('Internal server error') as Error & { statusCode: number }; - apiError.statusCode = 500; + const apiError = new Error('Internal server error') as Error & { httpStatus: number }; + apiError.httpStatus = 500; client.deleteAction.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/actions/delete.ts b/packages/cli/src/commands/actions/delete.ts index b77a596..01a0239 100644 --- a/packages/cli/src/commands/actions/delete.ts +++ b/packages/cli/src/commands/actions/delete.ts @@ -13,8 +13,8 @@ export function registerActionsDeleteAction(actionsCommand: Command): void { await client.deleteAction(config.appId, name); success(`Deleted ${name}`); } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 404) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 404) { error(`Action not found: ${name}`); hint('Run `canup actions list` to see available actions.'); } else { diff --git a/packages/cli/src/commands/actions/deploy.test.ts b/packages/cli/src/commands/actions/deploy.test.ts index b6d8e04..13a8374 100644 --- a/packages/cli/src/commands/actions/deploy.test.ts +++ b/packages/cli/src/commands/actions/deploy.test.ts @@ -321,9 +321,9 @@ describe('actions deploy command', () => { projectConfig.getActionsDir.mockReturnValue('/project/canup/actions'); const apiError = new Error('Conflict: action already deploying') as Error & { - statusCode: number; + httpStatus: number; }; - apiError.statusCode = 409; + apiError.httpStatus = 409; client.listActions.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -342,8 +342,8 @@ describe('actions deploy command', () => { test('handles 401 error with re-auth hint', async ({ client, output, processMocks }) => { projectConfig.getActionsDir.mockReturnValue('/project/canup/actions'); - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.listActions.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/actions/deploy.ts b/packages/cli/src/commands/actions/deploy.ts index 2239543..9f25859 100644 --- a/packages/cli/src/commands/actions/deploy.ts +++ b/packages/cli/src/commands/actions/deploy.ts @@ -142,9 +142,9 @@ export function registerActionsDeployAction(actionsCommand: Command): void { await deployAll(actionsDir, config.appId, client, remoteActions); } } catch (err) { - const e = err as Error & { statusCode?: number }; + const e = err as Error & { httpStatus?: number }; error(e.message); - if (e.statusCode === 401) { + if (e.httpStatus === 401) { hint('Run `canup init` to re-authenticate.'); } process.exit(1); diff --git a/packages/cli/src/commands/actions/invocations.test.ts b/packages/cli/src/commands/actions/invocations.test.ts index a702f20..7a86f6f 100644 --- a/packages/cli/src/commands/actions/invocations.test.ts +++ b/packages/cli/src/commands/actions/invocations.test.ts @@ -174,8 +174,8 @@ describe('actions invocations command', () => { }); test('handles 404 error on detail view', async ({ client, output, processMocks }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.getInvocationDetail.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -195,8 +195,8 @@ describe('actions invocations command', () => { }); test('handles 401 auth error', async ({ client, output, processMocks }) => { - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.listInvocations.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/actions/invocations.ts b/packages/cli/src/commands/actions/invocations.ts index cabfe79..6239eda 100644 --- a/packages/cli/src/commands/actions/invocations.ts +++ b/packages/cli/src/commands/actions/invocations.ts @@ -58,13 +58,13 @@ export function registerActionsInvocationsAction(actionsCommand: Command): void await showInvocationsList(client, config.appId, slug, limit, options.search); } } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 401) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 401) { error('Not authenticated.'); hint('Run `canup init` to re-authenticate.'); process.exit(1); } - if (e.statusCode === 404) { + if (e.httpStatus === 404) { error('Execution not found.'); hint('Run `canup actions invocations` to see recent executions.'); process.exit(1); diff --git a/packages/cli/src/commands/actions/remove.test.ts b/packages/cli/src/commands/actions/remove.test.ts index f7995a4..85910ec 100644 --- a/packages/cli/src/commands/actions/remove.test.ts +++ b/packages/cli/src/commands/actions/remove.test.ts @@ -32,8 +32,8 @@ describe('actions remove command', () => { }); test('handles 404 error with specific message', async ({ client, output, processMocks }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.deleteAction.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -51,8 +51,8 @@ describe('actions remove command', () => { }); test('handles generic error', async ({ client, output, processMocks }) => { - const apiError = new Error('Internal server error') as Error & { statusCode: number }; - apiError.statusCode = 500; + const apiError = new Error('Internal server error') as Error & { httpStatus: number }; + apiError.httpStatus = 500; client.deleteAction.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/actions/remove.ts b/packages/cli/src/commands/actions/remove.ts index 97e827a..8c6de5d 100644 --- a/packages/cli/src/commands/actions/remove.ts +++ b/packages/cli/src/commands/actions/remove.ts @@ -13,8 +13,8 @@ export function registerActionsRemoveAction(actionsCommand: Command): void { await client.deleteAction(config.appId, name); success(`Removed ${name}`); } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 404) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 404) { error(`Action not found: ${name}`); hint('Run `canup actions list` to see deployed actions.'); } else { diff --git a/packages/cli/src/commands/actions/run.test.ts b/packages/cli/src/commands/actions/run.test.ts index 7959a41..ea51bef 100644 --- a/packages/cli/src/commands/actions/run.test.ts +++ b/packages/cli/src/commands/actions/run.test.ts @@ -354,8 +354,8 @@ describe('actions run command', () => { output, processMocks, }) => { - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.listActionsWithScript.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -377,8 +377,8 @@ describe('actions run command', () => { output, processMocks, }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.listActionsWithScript.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/actions/run.ts b/packages/cli/src/commands/actions/run.ts index 1bb58a6..6ec32d9 100644 --- a/packages/cli/src/commands/actions/run.ts +++ b/packages/cli/src/commands/actions/run.ts @@ -45,16 +45,16 @@ export function registerActionsRunAction(actionsCommand: Command): void { displayTestResult(result, { success: 'Run succeeded', failure: 'Run failed' }); } catch (err) { - const statusCode = (err as { statusCode?: number }).statusCode; + const httpStatus = (err as { httpStatus?: number }).httpStatus; const message = err instanceof Error ? err.message : String(err); - if (statusCode === 401) { + if (httpStatus === 401) { error('Not authenticated.'); hint('Run `canup login` to re-authenticate.'); process.exit(1); } - if (statusCode === 404) { + if (httpStatus === 404) { error(`Action not found: ${name}`); hint('Deploy the action first with `canup actions deploy`.'); process.exit(1); diff --git a/packages/cli/src/commands/actions/test.test.ts b/packages/cli/src/commands/actions/test.test.ts index 6de4d7f..5b32b5a 100644 --- a/packages/cli/src/commands/actions/test.test.ts +++ b/packages/cli/src/commands/actions/test.test.ts @@ -410,8 +410,8 @@ describe('actions test command', () => { }) => { mockExistsSync.mockReturnValue(true); - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.testCode.mockRejectedValue(apiError); mockReadFileSync.mockReturnValue('code'); @@ -425,8 +425,8 @@ describe('actions test command', () => { test('handles 401 authentication error', async ({ client, output, processMocks }) => { mockExistsSync.mockReturnValue(true); - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.testCode.mockRejectedValue(apiError); mockReadFileSync.mockReturnValue('code'); diff --git a/packages/cli/src/commands/actions/test.ts b/packages/cli/src/commands/actions/test.ts index f01e405..f53a822 100644 --- a/packages/cli/src/commands/actions/test.ts +++ b/packages/cli/src/commands/actions/test.ts @@ -357,16 +357,16 @@ export function registerActionsTestAction(actionsCommand: Command): void { await runLocalTest(actionPath, language, params, context); } } catch (err) { - const statusCode = (err as { statusCode?: number }).statusCode; + const httpStatus = (err as { httpStatus?: number }).httpStatus; const message = err instanceof Error ? err.message : String(err); - if (statusCode === 401) { + if (httpStatus === 401) { error('Not authenticated.'); hint('Run `canup login` to re-authenticate.'); process.exit(1); } - if (statusCode === 404) { + if (httpStatus === 404) { error('App not found on server.'); hint('Check your project config or use local mode (without --remote).'); process.exit(1); diff --git a/packages/cli/src/commands/deps/add.test.ts b/packages/cli/src/commands/deps/add.test.ts index 1dca3be..8f588cd 100644 --- a/packages/cli/src/commands/deps/add.test.ts +++ b/packages/cli/src/commands/deps/add.test.ts @@ -169,8 +169,8 @@ describe('deps add command', () => { }); test('handles API error with 401 status', async ({ client, output, processMocks }) => { - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.addDeps.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/deps/add.ts b/packages/cli/src/commands/deps/add.ts index d664ae2..7ec4932 100644 --- a/packages/cli/src/commands/deps/add.ts +++ b/packages/cli/src/commands/deps/add.ts @@ -43,9 +43,9 @@ export function registerDepsAddAction(depsCommand: Command): void { label('Package', `${pkg.name}${pkg.version ? `@${pkg.version}` : ''}`); } } catch (err) { - const e = err as Error & { statusCode?: number }; + const e = err as Error & { httpStatus?: number }; error(e.message); - if (e.statusCode === 401) { + if (e.httpStatus === 401) { info('Run `canup init` to re-authenticate.'); } process.exit(1); diff --git a/packages/cli/src/commands/deps/clear.test.ts b/packages/cli/src/commands/deps/clear.test.ts index f8b9e79..e3c927a 100644 --- a/packages/cli/src/commands/deps/clear.test.ts +++ b/packages/cli/src/commands/deps/clear.test.ts @@ -51,8 +51,8 @@ describe('deps clear command', () => { }); test('handles API error', async ({ client, output, processMocks }) => { - const apiError = new Error('Server error') as Error & { statusCode: number }; - apiError.statusCode = 500; + const apiError = new Error('Server error') as Error & { httpStatus: number }; + apiError.httpStatus = 500; client.clearDeps.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -69,8 +69,8 @@ describe('deps clear command', () => { }); test('handles 401 error with re-auth hint', async ({ client, output, processMocks }) => { - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.clearDeps.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/deps/clear.ts b/packages/cli/src/commands/deps/clear.ts index 45c8037..799aab9 100644 --- a/packages/cli/src/commands/deps/clear.ts +++ b/packages/cli/src/commands/deps/clear.ts @@ -18,9 +18,9 @@ export function registerDepsClearAction(depsCommand: Command): void { await client.clearDeps(config.appId, language); success(`All ${language} packages cleared`); } catch (err) { - const e = err as Error & { statusCode?: number }; + const e = err as Error & { httpStatus?: number }; error(e.message); - if (e.statusCode === 401) { + if (e.httpStatus === 401) { info('Run `canup init` to re-authenticate.'); } process.exit(1); diff --git a/packages/cli/src/commands/deps/remove.test.ts b/packages/cli/src/commands/deps/remove.test.ts index d31d18e..c968a45 100644 --- a/packages/cli/src/commands/deps/remove.test.ts +++ b/packages/cli/src/commands/deps/remove.test.ts @@ -123,8 +123,8 @@ describe('deps remove command', () => { }); test('handles package not found (404)', async ({ client, output, processMocks }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.removeDep.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -221,8 +221,8 @@ describe('deps remove command', () => { }); test('shows auth hint on 401 error', async ({ client, output, processMocks }) => { - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.removeDep.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/deps/remove.ts b/packages/cli/src/commands/deps/remove.ts index 10bb8b7..721dfed 100644 --- a/packages/cli/src/commands/deps/remove.ts +++ b/packages/cli/src/commands/deps/remove.ts @@ -33,13 +33,13 @@ export function registerDepsRemoveAction(depsCommand: Command): void { }); } } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 404) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 404) { error(`Package not found.`); } else { error(e.message); } - if (e.statusCode === 401) { + if (e.httpStatus === 401) { info('Run `canup init` to re-authenticate.'); } process.exit(1); diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index 7da11ae..32c377b 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -177,8 +177,8 @@ describe('init command', () => { tokenStore.loadCredentials.mockReturnValue({ userKey: 'cnup_x', keyId: 'apikey_x' }); projectConfig.loadProjectConfig.mockReturnValue(null); - const apiError = new Error('Server error') as Error & { statusCode: number }; - apiError.statusCode = 500; + const apiError = new Error('Server error') as Error & { httpStatus: number }; + apiError.httpStatus = 500; client.registerApp.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -203,8 +203,8 @@ describe('init command', () => { name: 'App', }); - const apiError = new Error('Key creation failed') as Error & { statusCode: number }; - apiError.statusCode = 500; + const apiError = new Error('Key creation failed') as Error & { httpStatus: number }; + apiError.httpStatus = 500; client.createApiKey.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -268,8 +268,8 @@ describe('init command', () => { tokenStore.loadCredentials.mockReturnValue({ userKey: 'cnup_x', keyId: 'apikey_x' }); projectConfig.loadProjectConfig.mockReturnValue(null); - const apiError = new Error('Conflict') as Error & { statusCode: number }; - apiError.statusCode = 409; + const apiError = new Error('Conflict') as Error & { httpStatus: number }; + apiError.httpStatus = 409; client.registerApp.mockRejectedValue(apiError); const { Command } = await import('commander'); @@ -295,8 +295,8 @@ describe('init command', () => { }); projectConfig.loadProjectConfig.mockReturnValue(null); - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.registerApp.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e6a58f2..6d263f4 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -224,16 +224,16 @@ export function registerInitCommand(program: Command): void { } } } catch (err) { - const statusCode = (err as { statusCode?: number }).statusCode; + const httpStatus = (err as { httpStatus?: number }).httpStatus; const message = err instanceof Error ? err.message : String(err); - if (statusCode === 401) { + if (httpStatus === 401) { error('Session expired.'); hint('Run `canup login` to re-authenticate.'); process.exit(1); } - if (statusCode === 409) { + if (httpStatus === 409) { error('This Canva App ID is already registered by another user.'); process.exit(1); } diff --git a/packages/cli/src/commands/pull.test.ts b/packages/cli/src/commands/pull.test.ts index ed69a8c..aeb1a9a 100644 --- a/packages/cli/src/commands/pull.test.ts +++ b/packages/cli/src/commands/pull.test.ts @@ -257,8 +257,8 @@ describe('pull command', () => { test('handles 401 API error with re-auth hint', async ({ client, output, processMocks }) => { projectConfig.getActionsDir.mockReturnValue('/project/canup/actions'); - const apiError = new Error('Unauthorized') as Error & { statusCode: number }; - apiError.statusCode = 401; + const apiError = new Error('Unauthorized') as Error & { httpStatus: number }; + apiError.httpStatus = 401; client.listActionsWithScript.mockRejectedValue(apiError); await runPull(); diff --git a/packages/cli/src/commands/pull.ts b/packages/cli/src/commands/pull.ts index 34e8268..bc0314e 100644 --- a/packages/cli/src/commands/pull.ts +++ b/packages/cli/src/commands/pull.ts @@ -95,9 +95,9 @@ export function registerPullCommand(program: Command): void { `Pulled ${pulled} action${pulled !== 1 ? 's' : ''}${upToDate > 0 ? `, ${upToDate} up to date` : ''}${skipped > 0 ? `, ${skipped} skipped` : ''}`, ); } catch (err) { - const e = err as Error & { statusCode?: number }; + const e = err as Error & { httpStatus?: number }; error(e.message); - if (e.statusCode === 401) { + if (e.httpStatus === 401) { hint('Run `canup init` to re-authenticate.'); } process.exit(1); diff --git a/packages/cli/src/commands/secrets/delete.test.ts b/packages/cli/src/commands/secrets/delete.test.ts index d08fae6..a3d2c00 100644 --- a/packages/cli/src/commands/secrets/delete.test.ts +++ b/packages/cli/src/commands/secrets/delete.test.ts @@ -35,8 +35,8 @@ describe('secrets delete command', () => { }); test('handles 404 not found', async ({ client, output, processMocks }) => { - const apiError = new Error('Not found') as Error & { statusCode: number }; - apiError.statusCode = 404; + const apiError = new Error('Not found') as Error & { httpStatus: number }; + apiError.httpStatus = 404; client.deleteSecret.mockRejectedValue(apiError); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/secrets/delete.ts b/packages/cli/src/commands/secrets/delete.ts index 5e4f5a6..b148817 100644 --- a/packages/cli/src/commands/secrets/delete.ts +++ b/packages/cli/src/commands/secrets/delete.ts @@ -18,8 +18,8 @@ export function registerSecretsDeleteAction(secretsCommand: Command): void { hint('Warning: Lambda sync failed. Changes will be synced on next operation.'); } } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 404) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 404) { error(`Secret "${name}" not found.`); } else { error(e.message); diff --git a/packages/cli/src/commands/status.test.ts b/packages/cli/src/commands/status.test.ts index 91f3a6f..ec7c748 100644 --- a/packages/cli/src/commands/status.test.ts +++ b/packages/cli/src/commands/status.test.ts @@ -369,8 +369,8 @@ describe('status command', () => { test('handles 401 auth error', async ({ client, output, processMocks }) => { projectConfig.getActionsDir.mockReturnValue('/project/canup/actions'); - const authError = new Error('Unauthorized') as Error & { statusCode: number }; - authError.statusCode = 401; + const authError = new Error('Unauthorized') as Error & { httpStatus: number }; + authError.httpStatus = 401; client.getAppInfo.mockRejectedValue(authError); client.listActions.mockRejectedValue(authError); diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index 5e03b9b..61d8ee7 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -147,8 +147,8 @@ export function registerStatusCommand(program: Command): void { console.log(); } catch (err) { - const e = err as Error & { statusCode?: number }; - if (e.statusCode === 401) { + const e = err as Error & { httpStatus?: number }; + if (e.httpStatus === 401) { error('Not authenticated.'); hint('Run `canup login` to re-authenticate.'); process.exit(1); diff --git a/packages/cli/src/commands/stripe/connect.test.ts b/packages/cli/src/commands/stripe/connect.test.ts index a8e7701..f08c52e 100644 --- a/packages/cli/src/commands/stripe/connect.test.ts +++ b/packages/cli/src/commands/stripe/connect.test.ts @@ -1,6 +1,6 @@ import { describe, expect, vi } from 'vitest'; import { test, client, output, project, spinner } from '#test/fixtures.js'; -import { ApiError } from '../../errors.js'; +import { CanupError } from '@canup/contracts'; const { mockReadSecretInput } = vi.hoisted(() => ({ mockReadSecretInput: vi.fn() })); @@ -60,7 +60,7 @@ describe('stripe connect command', () => { test('shows error for invalid Stripe key', async ({ client, output, processMocks }) => { mockReadSecretInput.mockResolvedValue('sk_bad'); client.connectStripe.mockRejectedValue( - new ApiError(400, 'STRIPE_KEY_INVALID', 'Invalid API key'), + new CanupError('STRIPE_KEY_INVALID', 'Invalid API key', 400), ); const { Command } = await import('commander'); @@ -81,7 +81,7 @@ describe('stripe connect command', () => { test('shows error for permission issue', async ({ client, output, processMocks }) => { mockReadSecretInput.mockResolvedValue('sk_test_limited'); client.connectStripe.mockRejectedValue( - new ApiError(403, 'STRIPE_PERMISSION_ERROR', 'Missing: charges:read, subscriptions:read'), + new CanupError('STRIPE_PERMISSION_ERROR', 'Missing: charges:read, subscriptions:read', 403), ); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/stripe/connect.ts b/packages/cli/src/commands/stripe/connect.ts index ab268ab..e85ecbc 100644 --- a/packages/cli/src/commands/stripe/connect.ts +++ b/packages/cli/src/commands/stripe/connect.ts @@ -1,6 +1,6 @@ import type { Command } from 'commander'; import { requireClient } from '../../config/require-project.js'; -import { ApiError } from '../../errors.js'; +import { isCanupError } from '@canup/contracts'; import { readSecretInput } from '../../lib/input.js'; import { success, error } from '../../ui/output.js'; import { createSpinner } from '../../ui/spinner.js'; @@ -30,9 +30,9 @@ export function registerStripeConnectAction(stripeCommand: Command): void { } catch (err) { spin.fail('Connection failed'); - if (err instanceof ApiError && err.code === 'STRIPE_KEY_INVALID') { + if (isCanupError(err) && err.code === 'STRIPE_KEY_INVALID') { error('Invalid Stripe API key. Check that you copied the full key.'); - } else if (err instanceof ApiError && err.code === 'STRIPE_PERMISSION_ERROR') { + } else if (isCanupError(err) && err.code === 'STRIPE_PERMISSION_ERROR') { error(`Stripe key lacks required permissions: ${err.message}`); } else { error(err instanceof Error ? err.message : String(err)); diff --git a/packages/cli/src/commands/whoami.test.ts b/packages/cli/src/commands/whoami.test.ts index ac90e6c..9fa8495 100644 --- a/packages/cli/src/commands/whoami.test.ts +++ b/packages/cli/src/commands/whoami.test.ts @@ -89,8 +89,8 @@ describe('whoami command', () => { test('prints session expired on 401 error', async ({ client: c, processMocks }) => { tokenStore.loadCredentials.mockReturnValue({ userKey: 'cnup_x', keyId: 'apikey_x' }); - const error = new Error('Invalid or expired session') as Error & { statusCode: number }; - error.statusCode = 401; + const error = new Error('Invalid or expired session') as Error & { httpStatus: number }; + error.httpStatus = 401; c.getMe.mockRejectedValue(error); const { Command } = await import('commander'); diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index 9e13724..35b8e69 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -28,9 +28,9 @@ export function registerWhoamiCommand(program: Command): void { console.log(`Email: ${me.email}`); console.log(`Member since: ${new Date(me.createdAt).toLocaleDateString()}`); } catch (err) { - const statusCode = (err as { statusCode?: number }).statusCode; + const httpStatus = (err as { httpStatus?: number }).httpStatus; - if (statusCode === 401) { + if (httpStatus === 401) { console.error('Session expired. Run `canup login` to re-authenticate.'); process.exit(1); } diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts deleted file mode 100644 index 45611e2..0000000 --- a/packages/cli/src/errors.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** An error from the CanUp API: the HTTP transport status plus the server's domain error code. */ -export class ApiError extends Error { - readonly statusCode: number; - readonly code: string; - - constructor(statusCode: number, code: string, message: string) { - super(message); - this.name = 'ApiError'; - this.statusCode = statusCode; - this.code = code; - } -} diff --git a/packages/ui/src/components/ActionButton.test.tsx b/packages/ui/src/components/ActionButton.test.tsx index 5a9053a..b5fa93b 100644 --- a/packages/ui/src/components/ActionButton.test.tsx +++ b/packages/ui/src/components/ActionButton.test.tsx @@ -5,7 +5,7 @@ import { renderWithCanva } from '#test/setup/ui.js'; import { ActionButton } from './ActionButton.js'; import { useAction } from '../hooks/use-action.js'; import { useCredits } from '../hooks/use-credits.js'; -import { CanupError } from '../errors.js'; +import { CanupError } from '@canup/contracts'; vi.mock('../hooks/use-action.js', () => ({ useAction: vi.fn(), diff --git a/packages/ui/src/components/ActionButton.tsx b/packages/ui/src/components/ActionButton.tsx index 3f47554..a54f85e 100644 --- a/packages/ui/src/components/ActionButton.tsx +++ b/packages/ui/src/components/ActionButton.tsx @@ -3,7 +3,7 @@ import { Button, Rows } from '@canva/app-ui-kit'; import { useAction } from '../hooks/use-action.js'; import { useCredits } from '../hooks/use-credits.js'; import { ActionCredits } from './ActionCredits.js'; -import { type CanupError, toCanupError } from '../errors.js'; +import { type CanupError, toCanupError } from '@canup/contracts'; type CanvaButtonProps = ComponentProps; diff --git a/packages/ui/src/errors.ts b/packages/ui/src/errors.ts deleted file mode 100644 index cad7e28..0000000 --- a/packages/ui/src/errors.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class CanupError extends Error { - readonly code: string; - readonly details?: Record; - - constructor(code: string, message: string, details?: Record) { - super(message); - this.name = 'CanupError'; - this.code = code; - this.details = details; - } -} - -/** Normalize an unknown caught value into a CanupError. */ -export function toCanupError(err: unknown): CanupError { - if (err instanceof CanupError) return err; - return new CanupError('NETWORK_ERROR', err instanceof Error ? err.message : 'Request failed'); -} diff --git a/packages/ui/src/hooks/use-action.test.ts b/packages/ui/src/hooks/use-action.test.ts index feedd57..bb44eba 100644 --- a/packages/ui/src/hooks/use-action.test.ts +++ b/packages/ui/src/hooks/use-action.test.ts @@ -3,7 +3,7 @@ import { renderHook, act, waitFor, cleanup } from '@testing-library/react'; import { useAction } from './use-action.js'; import { runAction } from '../internal/api-client.js'; import { queryClient, creditKey } from '../internal/query.js'; -import { CanupError } from '../errors.js'; +import { CanupError } from '@canup/contracts'; import type { CreditBalance } from '@canup/contracts'; vi.mock('../internal/api-client.js', () => ({ @@ -122,7 +122,7 @@ describe('useAction', () => { expect(result.current.error!.code).toBe('ACTION_NOT_FOUND'); }); - test('on non-CanupError, error is wrapped in CanupError("NETWORK_ERROR")', async () => { + test('on non-CanupError, error is wrapped in CanupError("TRANSPORT_ERROR")', async () => { mockRunAction.mockRejectedValue(new Error('Network timeout')); const { result } = renderHook(() => useAction('my-action')); @@ -138,7 +138,7 @@ describe('useAction', () => { await waitFor(() => { expect(result.current.error).toBeInstanceOf(CanupError); }); - expect(result.current.error!.code).toBe('NETWORK_ERROR'); + expect(result.current.error!.code).toBe('TRANSPORT_ERROR'); expect(result.current.error!.message).toBe('Network timeout'); }); diff --git a/packages/ui/src/hooks/use-action.ts b/packages/ui/src/hooks/use-action.ts index e54c776..9b0e0b1 100644 --- a/packages/ui/src/hooks/use-action.ts +++ b/packages/ui/src/hooks/use-action.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { useMutation } from '@tanstack/react-query'; import { queryClient, creditKey } from '../internal/query.js'; import { runAction } from '../internal/api-client.js'; -import { type CanupError, toCanupError } from '../errors.js'; +import { type CanupError, toCanupError } from '@canup/contracts'; export interface UseActionResult { execute: (params?: Record) => Promise; diff --git a/packages/ui/src/hooks/use-credits.test.ts b/packages/ui/src/hooks/use-credits.test.ts index bd09c5a..fe4b298 100644 --- a/packages/ui/src/hooks/use-credits.test.ts +++ b/packages/ui/src/hooks/use-credits.test.ts @@ -141,7 +141,7 @@ describe('useCredits', () => { expect(result.current.data).toBeNull(); expect(result.current.exhausted).toBe(false); expect(result.current.error).not.toBeNull(); - expect(result.current.error!.code).toBe('NETWORK_ERROR'); + expect(result.current.error!.code).toBe('TRANSPORT_ERROR'); expect(result.current.error!.message).toBe('Network error'); }); diff --git a/packages/ui/src/hooks/use-credits.ts b/packages/ui/src/hooks/use-credits.ts index afb2202..25331cd 100644 --- a/packages/ui/src/hooks/use-credits.ts +++ b/packages/ui/src/hooks/use-credits.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { queryClient, creditKey } from '../internal/query.js'; import { fetchCredits } from '../internal/api-client.js'; import { acquire } from '../internal/realtime.js'; -import { type CanupError, toCanupError } from '../errors.js'; +import { type CanupError, toCanupError } from '@canup/contracts'; import type { CreditBalance } from '@canup/contracts'; export interface UseCreditsResult { diff --git a/packages/ui/src/hooks/use-customer.test.ts b/packages/ui/src/hooks/use-customer.test.ts index 1959fec..c3486cf 100644 --- a/packages/ui/src/hooks/use-customer.test.ts +++ b/packages/ui/src/hooks/use-customer.test.ts @@ -143,7 +143,7 @@ describe('useCustomer', () => { expect(result.current.appName).toBeNull(); expect(result.current.error).not.toBeNull(); - expect(result.current.error!.code).toBe('NETWORK_ERROR'); + expect(result.current.error!.code).toBe('TRANSPORT_ERROR'); }); test('warns when the fetch fails, so the silent render-nothing surface is diagnosable', async () => { diff --git a/packages/ui/src/hooks/use-customer.ts b/packages/ui/src/hooks/use-customer.ts index c685297..63b28a0 100644 --- a/packages/ui/src/hooks/use-customer.ts +++ b/packages/ui/src/hooks/use-customer.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { queryClient, customerKey } from '../internal/query.js'; import { fetchCustomer } from '../internal/api-client.js'; import { acquire } from '../internal/realtime.js'; -import { type CanupError, toCanupError } from '../errors.js'; +import { type CanupError, toCanupError } from '@canup/contracts'; import type { Customer, SubscriptionStatus } from '@canup/contracts'; export interface UseCustomerResult { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6c5cf4f..b0ce264 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -18,4 +18,4 @@ export type { UseCustomerResult } from './hooks/use-customer.js'; // — it would collide with the `` component. Consumers who // need the union can reach it via `Customer['subscriptionStatus']`. export type { CreditBalance, Customer } from '@canup/contracts'; -export { CanupError } from './errors.js'; +export { CanupError } from '@canup/contracts'; diff --git a/packages/ui/src/internal/api-client.test.ts b/packages/ui/src/internal/api-client.test.ts index 265c373..cf658c6 100644 --- a/packages/ui/src/internal/api-client.test.ts +++ b/packages/ui/src/internal/api-client.test.ts @@ -1,7 +1,7 @@ import { describe, test as baseTest, expect, vi, beforeAll, afterAll, afterEach } from 'vitest'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { CanupError } from '../errors.js'; +import { CanupError } from '@canup/contracts'; function createMockJwt(exp: number): string { const header = btoa(JSON.stringify({ alg: 'RS256' })); diff --git a/packages/ui/src/internal/api-client.ts b/packages/ui/src/internal/api-client.ts index 54bd673..e5c5e42 100644 --- a/packages/ui/src/internal/api-client.ts +++ b/packages/ui/src/internal/api-client.ts @@ -1,12 +1,6 @@ import { getJwt } from './jwt-cache.js'; -import { CanupError, toCanupError } from '../errors.js'; -import type { - ApiResponse, - CreditBalance, - Customer, - RunResult, - SubscribeLinkResult, -} from '@canup/contracts'; +import { toCanupError, unwrapResponse } from '@canup/contracts'; +import type { CreditBalance, Customer, RunResult, SubscribeLinkResult } from '@canup/contracts'; declare global { var __canup_url: string | undefined; @@ -32,20 +26,7 @@ async function request(path: string, options?: RequestInit): Promise { throw toCanupError(err); } - let json: ApiResponse; - try { - json = (await res.json()) as ApiResponse; - } catch { - throw new CanupError('HTTP_ERROR', `Server returned ${res.status}`, { - status: res.status, - }); - } - - if (!json.ok) { - throw new CanupError(json.error.code, json.error.message); - } - - return json.data; + return unwrapResponse(res); } export const fetchCredits = (action: string) => request(`/run/${action}/credits`);