diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 2b9622d37..e50b1b4de 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -28,6 +28,50 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 + env: + # Database Configuration + DATABASE_URL: postgresql://test:test@localhost:5433/echo_integration_test + + # Service URLs + ECHO_CONTROL_URL: http://localhost:3001 + ECHO_DATA_SERVER_URL: http://localhost:3069 + + # Authentication Secrets + JWT_SECRET: ${{ secrets.TEST_JWT_SECRET }} + JWT_ISSUER: http://localhost:3001 + JWT_AUDIENCE: echo-proxy + + # Auth.js Configuration + AUTH_SECRET: ${{ secrets.TEST_AUTH_SECRET }} + + # Integration Test JWTs + INTEGRATION_TEST_JWT: ${{ secrets.INTEGRATION_TEST_JWT }} + INTEGRATION_TEST_JWT_USER_2: ${{ secrets.INTEGRATION_TEST_JWT_USER_2 }} + + # API Keys for Echo Server + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + # OAuth Configuration + OAUTH_REFRESH_TOKEN_EXPIRY_SECONDS: 2592000 + OAUTH_ACCESS_TOKEN_EXPIRY_SECONDS: 86400 + + # Environment Flags + CI: true + NODE_ENV: test + INTEGRATION_TEST_MODE: true + + NEXT_PUBLIC_ECHO_APP_ID: 74d9c979-e036-4e43-904f-32d214b361fc + + # Optional Stripe Configuration + STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} + STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }} + + # Test Configuration + HEADLESS: true + LOG_LEVEL: error + services: postgres: image: postgres:15-alpine @@ -88,6 +132,9 @@ jobs: INTEGRATION_TEST_JWT=${{ secrets.INTEGRATION_TEST_JWT }} INTEGRATION_TEST_JWT_USER_2=${{ secrets.INTEGRATION_TEST_JWT_USER_2 }} + # Next.js Configuration + NEXT_PUBLIC_ECHO_APP_ID=74d9c979-e036-4e43-904f-32d214b361fc + # API Keys for Echo Server OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} @@ -120,34 +167,10 @@ jobs: - name: Setup Prisma schema working-directory: packages/app/control - env: - DATABASE_URL: postgresql://test:test@localhost:5433/echo_integration_test run: pnpm prisma:generate - name: Setup integration test environment working-directory: packages/tests/integration - env: - DATABASE_URL: postgresql://test:test@localhost:5433/echo_integration_test - ECHO_CONTROL_URL: http://localhost:3001 - ECHO_DATA_SERVER_URL: http://localhost:3069 - JWT_SECRET: ${{ secrets.TEST_JWT_SECRET }} - JWT_ISSUER: http://localhost:3001 - JWT_AUDIENCE: echo-proxy - INTEGRATION_TEST_JWT: ${{ secrets.INTEGRATION_TEST_JWT }} - INTEGRATION_TEST_JWT_USER_2: ${{ secrets.INTEGRATION_TEST_JWT_USER_2 }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OAUTH_REFRESH_TOKEN_EXPIRY_SECONDS: 2592000 - OAUTH_ACCESS_TOKEN_EXPIRY_SECONDS: 86400 - CI: true - NODE_ENV: test - INTEGRATION_TEST_MODE: true - STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} - STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} - STRIPE_WEBHOOK_SECRET: ${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }} - HEADLESS: true - LOG_LEVEL: error - AUTH_SECRET: ${{ secrets.TEST_AUTH_SECRET }} run: | chmod +x scripts/setup-integration-env.sh pnpm run env:setup @@ -188,6 +211,50 @@ jobs: needs: integration-tests if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-e2e') + env: + # Database Configuration + DATABASE_URL: postgresql://test:test@postgres-test:5432/echo_integration_test + + # Service URLs + ECHO_CONTROL_URL: http://echo-control-test:3000 + ECHO_DATA_SERVER_URL: http://echo-data-server-test:3069 + + # Authentication Secrets + JWT_SECRET: ${{ secrets.TEST_JWT_SECRET }} + JWT_ISSUER: http://echo-control-test:3000 + JWT_AUDIENCE: echo-proxy + + # Auth.js Configuration + AUTH_SECRET: ${{ secrets.TEST_AUTH_SECRET }} + + # Integration Test JWTs + INTEGRATION_TEST_JWT: ${{ secrets.INTEGRATION_TEST_JWT }} + INTEGRATION_TEST_JWT_USER_2: ${{ secrets.INTEGRATION_TEST_JWT_USER_2 }} + + # API Keys for Echo Server + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + # OAuth Configuration + OAUTH_REFRESH_TOKEN_EXPIRY_SECONDS: 2592000 + OAUTH_ACCESS_TOKEN_EXPIRY_SECONDS: 86400 + + # Environment Flags + CI: true + NODE_ENV: test + INTEGRATION_TEST_MODE: true + + # Optional Stripe Configuration + STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} + STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }} + + NEXT_PUBLIC_ECHO_APP_ID: 74d9c979-e036-4e43-904f-32d214b361fc + + # Test Configuration + HEADLESS: true + LOG_LEVEL: error + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/publish-sdks.yml b/.github/workflows/publish-sdks.yml index e560c1e31..b4ab36525 100644 --- a/.github/workflows/publish-sdks.yml +++ b/.github/workflows/publish-sdks.yml @@ -80,7 +80,7 @@ jobs: if [ "$FORCE_PUBLISH" = "true" ]; then echo "⚠️ Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on npm, but force_publish is enabled" echo "🚀 Publishing $PACKAGE_NAME to npm..." - npm publish --access public + pnpm publish --access public --no-git-checks echo "published=true" >> $GITHUB_OUTPUT echo "✅ Successfully published $PACKAGE_NAME" else @@ -126,7 +126,7 @@ jobs: if [ "$FORCE_PUBLISH" = "true" ]; then echo "⚠️ Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on npm, but force_publish is enabled" echo "🚀 Publishing $PACKAGE_NAME to npm..." - npm publish --access public + pnpm publish --access public --no-git-checks echo "published=true" >> $GITHUB_OUTPUT echo "✅ Successfully published $PACKAGE_NAME" else @@ -170,7 +170,7 @@ jobs: if [ "$FORCE_PUBLISH" = "true" ]; then echo "⚠️ Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on npm, but force_publish is enabled" echo "🚀 Publishing $PACKAGE_NAME to npm..." - npm publish --access public + pnpm publish --access public --no-git-checks echo "published=true" >> $GITHUB_OUTPUT echo "✅ Successfully published $PACKAGE_NAME" else @@ -214,7 +214,7 @@ jobs: if [ "$FORCE_PUBLISH" = "true" ]; then echo "⚠️ Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on npm, but force_publish is enabled" echo "🚀 Publishing $PACKAGE_NAME to npm..." - npm publish --access public + pnpm publish --access public --no-git-checks echo "published=true" >> $GITHUB_OUTPUT echo "✅ Successfully published $PACKAGE_NAME" else diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index a7a440e7e..7623c33c3 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -35,6 +35,50 @@ jobs: next-version: ${{ steps.get-versions.outputs.next-version }} echo-start-version: ${{ steps.get-versions.outputs.echo-start-version }} + env: + # Database Configuration + DATABASE_URL: postgresql://test:test@localhost:5433/echo_integration_test + + # Service URLs + ECHO_CONTROL_URL: http://localhost:3001 + ECHO_DATA_SERVER_URL: http://localhost:3069 + + # Authentication Secrets + JWT_SECRET: ${{ secrets.TEST_JWT_SECRET }} + JWT_ISSUER: http://localhost:3001 + JWT_AUDIENCE: echo-proxy + + # Auth.js Configuration + AUTH_SECRET: ${{ secrets.TEST_AUTH_SECRET }} + + # Integration Test JWTs + INTEGRATION_TEST_JWT: ${{ secrets.INTEGRATION_TEST_JWT }} + INTEGRATION_TEST_JWT_USER_2: ${{ secrets.INTEGRATION_TEST_JWT_USER_2 }} + + # API Keys for Echo Server + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + # OAuth Configuration + OAUTH_REFRESH_TOKEN_EXPIRY_SECONDS: 2592000 + OAUTH_ACCESS_TOKEN_EXPIRY_SECONDS: 86400 + + # Environment Flags + CI: true + NODE_ENV: test + INTEGRATION_TEST_MODE: true + + NEXT_PUBLIC_ECHO_APP_ID: 74d9c979-e036-4e43-904f-32d214b361fc + + # Optional Stripe Configuration + STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} + STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }} + + # Test Configuration + HEADLESS: true + LOG_LEVEL: error + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/Dockerfile.railway b/Dockerfile.railway index 418f05e1b..3ca31d761 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -74,6 +74,7 @@ COPY pnpm-lock.yaml ./ COPY pnpm-workspace.yaml ./ COPY package.json ./ COPY tsconfig.base.json ./ +COPY turbo.json ./ # Copy all project files first COPY packages/app/control/ ./packages/app/control/ @@ -82,28 +83,21 @@ COPY packages/sdk/ts/ ./packages/sdk/ts/ COPY packages/sdk/next/ ./packages/sdk/next/ COPY packages/sdk/react/ ./packages/sdk/react/ -# Install dependencies for both projects (including dev dependencies for build) -WORKDIR /app/packages/app/control +# Install all workspace dependencies +WORKDIR /app RUN pnpm install -WORKDIR /app/packages/app/server -RUN pnpm install +# Build all packages in correct dependency order using Turbo +WORKDIR /app +RUN pnpm run build -# Step 1: Build echo-control and generate Prisma client +# Generate Prisma client and copy schema for server WORKDIR /app/packages/app/control RUN pnpm run prisma:generate -RUN pnpm run build - -WORKDIR /app/packages/sdk/ts -RUN pnpm install -RUN pnpm run build WORKDIR /app/packages/app/server RUN pnpm run copy-prisma -# Step 3: Build echo-server -RUN pnpm run build - # Set NODE_OPTIONS to enable OpenTelemetry for echo-server # Step 4: Install production dependencies only diff --git a/package.json b/package.json index 52e74e2de..a98b2deae 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "packageManager": "pnpm@10.11.0", "scripts": { "dev": "turbo run dev --filter=echo-control --filter=echo-server", - "build": "pnpm run --recursive build", + "build": "turbo run build --env-mode=loose", + "prepare": "SKIP_ENV_VALIDATION=true turbo run build --env-mode=loose --filter=./packages/sdk/* --filter=!echo-next-template --filter=!echo-vite-template --filter=!registry", "test:unit": "pnpm -C packages/sdk/ts run build && pnpm -r --filter=!packages/app/server run test:unit", "test:integration": "pnpm -C packages/tests/integration test", "test:all": "pnpm run test:unit && pnpm run test:integration", @@ -14,7 +15,10 @@ "lint:fix": "pnpm run --recursive lint:fix", "type-check": "pnpm run --recursive type-check", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"" + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", + "publish:test": "pnpm -r publish --dry-run --no-git-checks", + "publish:sdks": "pnpm --filter @merit-systems/echo-typescript-sdk publish --no-git-checks && pnpm --filter @merit-systems/echo-react-sdk publish --no-git-checks && pnpm --filter @merit-systems/echo-next-sdk publish --no-git-checks && pnpm --filter echo-start publish --no-git-checks", + "publish:sdks:test": "pnpm --filter @merit-systems/echo-typescript-sdk publish --dry-run --no-git-checks && pnpm --filter @merit-systems/echo-react-sdk publish --dry-run --no-git-checks && pnpm --filter @merit-systems/echo-next-sdk publish --dry-run --no-git-checks && pnpm --filter echo-start publish --dry-run --no-git-checks" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/packages/app/control/knip.json b/packages/app/control/knip.json index c2e44cbc6..56d5fc475 100644 --- a/packages/app/control/knip.json +++ b/packages/app/control/knip.json @@ -6,12 +6,7 @@ "src/app/**/route.{js,jsx,ts,tsx}", "src/mdx-components.{js,jsx,ts,tsx}" ], - "ignoreDependencies": [ - "eslint-config-next", - "husky", - "tw-animate-css", - "@merit-systems/echo-typescript-sdk" - ], + "ignoreDependencies": ["eslint-config-next", "husky", "tw-animate-css"], "ignore": [ "/scripts/**", "/source.config.ts", diff --git a/packages/app/control/package.json b/packages/app/control/package.json index 070241dc2..3d1224516 100644 --- a/packages/app/control/package.json +++ b/packages/app/control/package.json @@ -5,7 +5,7 @@ "scripts": { "predev": "docker-compose -f docker-local-db.yml up -d && sleep 3 && prisma generate && prisma migrate deploy", "dev": "next dev --turbopack", - "prebuild": "prisma generate && npm run generate-api-types", + "prebuild": "prisma generate", "build": "next build", "generate-api-types": "tsx scripts/generate-api-types.ts", "start": "next start", @@ -31,7 +31,6 @@ "@coinbase/x402": "^0.5.0", "@hookform/resolvers": "^5.2.1", "@icons-pack/react-simple-icons": "^13.7.0", - "@merit-systems/echo-typescript-sdk": "1.0.14", "@merit-systems/sdk": "0.0.8", "@number-flow/react": "^0.5.10", "@opentelemetry/api": "^1.9.0", diff --git a/packages/app/control/scripts/generate-api-types.ts b/packages/app/control/scripts/generate-api-types.ts index 6c6fc4a6b..c1718c42b 100644 --- a/packages/app/control/scripts/generate-api-types.ts +++ b/packages/app/control/scripts/generate-api-types.ts @@ -21,13 +21,24 @@ interface GeneratedType { /** * Convert file path to API route path * e.g., "apps/[id]/route.ts" -> "/apps/{id}" + * e.g., "oauth/authorize/route.ts" -> "/oauth/authorize" */ function filePathToRoutePath(filePath: string): string { - const relativePath = filePath - .replace(/^.*\/api\/v1\//, '') + let relativePath = filePath + .replace(/^.*\/api\//, '') .replace(/\/route\.ts$/, '') .replace(/\[([^\]]+)\]/g, '{$1}'); // Convert [id] to {id} + // Handle OAuth routes differently - they don't need the v1 prefix + if (relativePath.startsWith('oauth/')) { + return `/${relativePath}`; + } + + // For v1 routes, remove the v1 prefix + if (relativePath.startsWith('v1/')) { + relativePath = relativePath.replace(/^v1\//, ''); + } + return relativePath ? `/${relativePath}` : ''; } @@ -179,7 +190,7 @@ function analyzeRouteFile( * Generate the complete types file content with resolved types */ function generateTypesFileContent(generatedTypes: GeneratedType[]): string { - const imports = `import { OriginalRouteHandler } from '../app/api/_utils/types'; + const imports = `import { OriginalRouteHandler } from '../lib/api/types'; import { z } from 'zod'; // Auto-generated API response types @@ -187,12 +198,14 @@ import { z } from 'zod'; // Do not edit this file manually - it will be overwritten // Utility type to extract types from route handlers -type ExtractRouteHandlerTypes = T extends OriginalRouteHandler +type ExtractRouteHandlerTypes = T extends OriginalRouteHandler ? { params: TParams extends z.Schema ? z.infer : TParams; query: TQuery extends z.Schema ? z.infer : TQuery; body: TBody extends z.Schema ? z.infer : TBody; - response: TResponse + response: TResponse; + serverErrorBody: TServerErrorBody; + internalErrorBody: TInternalErrorBody; } : never; @@ -218,7 +231,24 @@ type ExtractRouteHandlerTypes = T extends OriginalRouteHandler = T extends OriginalRouteHandler +type ExtractRouteHandlerTypes = T extends OriginalRouteHandler ? { params: TParams extends z.Schema ? z.infer : TParams; query: TQuery extends z.Schema ? z.infer : TQuery; body: TBody extends z.Schema ? z.infer : TBody; - response: TResponse + response: TResponse; + serverErrorBody: TServerErrorBody; + internalErrorBody: TInternalErrorBody; } : never; @@ -368,7 +400,24 @@ type ExtractRouteHandlerTypes = T extends OriginalRouteHandler { const app = await getApp(query.client_id); if (!app) { - return NextResponse.json( - { error: 'not_found', error_description: 'Echo app not found' }, - { status: 404 } - ); + return OAuthRouteError({ + error: OAuthErrorType.INVALID_CLIENT, + error_description: 'Echo app not found', + }); } const session = await auth(); @@ -38,35 +73,26 @@ export const GET = createZodRoute() if (query.prompt === 'none') { if (!env.INTEGRATION_TEST_MODE) { - return NextResponse.json( - { - error: 'invalid_request', - message: 'prompt=none is not supported', - }, - { status: 400 } - ); + return OAuthRouteError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'prompt=none is not supported', + }); } if (!isValidRedirectUri(query.redirect_uri, app.authorizedCallbackUrls)) { - return NextResponse.json( - { - error: 'invalid_request', - message: 'redirect_uri is not authorized for this app', - }, - { status: 400 } - ); + return OAuthRouteError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'redirect_uri is not authorized for this app', + }); } const redirectUrl = await getAuthorizationRedirect(query); if (!redirectUrl) { - return NextResponse.json( - { - error: 'unauthorized', - error_description: 'User not authenticated', - }, - { status: 400 } - ); + return OAuthRouteError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'User not authenticated', + }); } return NextResponse.redirect(redirectUrl.toString(), 302); diff --git a/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-error.ts b/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-error.ts new file mode 100644 index 000000000..e0b6e1b61 --- /dev/null +++ b/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-error.ts @@ -0,0 +1,24 @@ +export enum OAuthErrorType { + INVALID_REQUEST = 'invalid_request', + INVALID_CLIENT = 'invalid_client', + ACCESS_DENIED = 'access_denied', + UNSUPPORTED_RESPONSE_TYPE = 'unsupported_response_type', + INVALID_SCOPE = 'invalid_scope', + SERVER_ERROR = 'server_error', + TEMPORARILY_UNAVAILABLE = 'temporarily_unavailable', + INVALID_GRANT = 'invalid_grant', + UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type', +} + +export type OAuthErrorBody = { + error: OAuthErrorType; + error_description?: string; +}; + +export class OAuthError extends Error { + readonly body: OAuthErrorBody; + constructor(body: OAuthErrorBody) { + super(body.error); + this.body = body; + } +} diff --git a/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-route.ts b/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-route.ts new file mode 100644 index 000000000..b22b57fc0 --- /dev/null +++ b/packages/app/control/src/app/(auth)/(oauth)/_lib/oauth-route.ts @@ -0,0 +1,50 @@ +import { createZodRoute } from '@/lib/api/create-route'; +import { OAuthError, OAuthErrorBody, OAuthErrorType } from './oauth-error'; +import { NextResponse } from 'next/server'; +import { + HandleInternalErrorFn, + HandlerServerErrorFn, + InternalRouteHandlerError, +} from '@/lib/api/types'; + +const handleOAuthServerError: HandlerServerErrorFn = ( + error: Error +) => { + if (error instanceof OAuthError) { + return NextResponse.json(error.body, { status: 400 }); + } + return NextResponse.json( + { error: OAuthErrorType.SERVER_ERROR, error_description: error.message }, + { status: 500 } + ); +}; + +const handleOAuthInternalError: HandleInternalErrorFn = ( + error: InternalRouteHandlerError +) => { + const { errors } = error.body; + if (!errors?.length) { + return NextResponse.json( + { error: OAuthErrorType.SERVER_ERROR, error_description: error.message }, + { status: 500 } + ); + } + const firstError = errors[0]; + return NextResponse.json(JSON.parse(firstError.message) as OAuthErrorBody, { + status: 400, + }); +}; + +export const oauthRoute = createZodRoute({ + handleServerError: handleOAuthServerError, + handleInternalError: handleOAuthInternalError, +}); + +export const OAuthRouteError = (error: OAuthErrorBody) => { + return NextResponse.json(error, { + status: error.error === OAuthErrorType.SERVER_ERROR ? 500 : 400, + }); +}; + +export const oauthValidationError = (error: OAuthErrorBody) => + JSON.stringify(error); diff --git a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts index 39fd9a6c1..89fd43ef0 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts +++ b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts @@ -18,17 +18,55 @@ import { logger } from '@/logger'; import { env } from '@/env'; import type { TokenMetadata } from './types'; +import { + OAuthError, + OAuthErrorType, +} from '@/app/(auth)/(oauth)/_lib/oauth-error'; +import { oauthValidationError } from '@/app/(auth)/(oauth)/_lib/oauth-route'; export const handleIssueTokenSchema = z.object({ - redirect_uri: z.url('redirect_uri must be a valid URL'), - client_id: z.uuid('client_id must be a valid UUID'), - code: z.string(), + redirect_uri: z.url({ + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'redirect_uri must be a valid URL', + }), + }), + client_id: z.uuid({ + error: oauthValidationError({ + error: OAuthErrorType.INVALID_CLIENT, + error_description: 'client_id must be a valid UUID', + }), + }), + code: z.string({ + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'code must be a string', + }), + }), code_verifier: z - .string() - .min(43, 'code_verifier must be at least 43 characters') - .max(128, 'code_verifier must be at most 128 characters') - .regex(/^[A-Za-z0-9_-]+$/, { - message: 'code_verifier must be base64url encoded', + .string({ + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'code_verifier must be a string', + }), + }) + .min(43, { + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'code_verifier must be at least 43 characters', + }), + }) + .max(128, { + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'code_verifier must be at most 128 characters', + }), + }) + .regex(/^[A-Za-z0-9._~-]+$/, { + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'code_verifier contains invalid characters', + }), }), }); @@ -50,7 +88,12 @@ export async function handleIssueToken( const { payload } = await jwtVerify( code, new TextEncoder().encode(env.OAUTH_CODE_SIGNING_JWT_SECRET) - ); + ).catch(() => { + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'Invalid authorization code', + }); + }); const { client_id: codeClientId, redirect_uri: codeRedirectUri, @@ -61,7 +104,10 @@ export async function handleIssueToken( /* 4️⃣ Validate the authorization code data */ if (codeClientId !== client_id || codeRedirectUri !== redirect_uri) { - throw new Error('Authorization code does not match request parameters'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'Authorization code does not match request parameters', + }); } /* 5️⃣ Verify PKCE code challenge */ @@ -70,7 +116,10 @@ export async function handleIssueToken( .digest('base64url'); if (codeVerifierHash !== code_challenge) { - throw new Error('PKCE verification failed'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'PKCE verification failed', + }); } const user = await db.user.findUnique({ @@ -78,7 +127,10 @@ export async function handleIssueToken( }); if (!user) { - throw new Error('User not found'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'User not found', + }); } /* 7️⃣ Find and validate the Echo app (client) */ @@ -90,14 +142,18 @@ export async function handleIssueToken( }); if (!app) { - throw new Error('App not found'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'App not found', + }); } /* 8️⃣ Validate redirect_uri against authorized callback URLs */ if (!isValidRedirectUri(redirect_uri, app.authorizedCallbackUrls)) { - throw new Error( - `Redirect URI is not authorized for this app: ${redirect_uri}` - ); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: `Redirect URI is not authorized for this app: ${redirect_uri}`, + }); } /* 9️⃣ Ensure the user has access to this Echo app */ @@ -126,7 +182,10 @@ export async function handleIssueToken( }); userRole = AppRole.CUSTOMER; } catch { - throw new Error('Error creating app membership'); + throw new OAuthError({ + error: OAuthErrorType.SERVER_ERROR, + error_description: 'Error creating app membership', + }); } } diff --git a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/refresh.ts b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/refresh.ts index f1b919279..f28c60c0d 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/refresh.ts +++ b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/refresh.ts @@ -15,9 +15,19 @@ import { createEchoAccessJwt } from '@/lib/access-token'; import type { Prisma } from '@/generated/prisma'; import type { TokenMetadata } from './types'; +import { oauthValidationError } from '@/app/(auth)/(oauth)/_lib/oauth-route'; +import { + OAuthError, + OAuthErrorType, +} from '@/app/(auth)/(oauth)/_lib/oauth-error'; export const handleRefreshTokenSchema = z.object({ - refresh_token: z.string(), + refresh_token: z.string({ + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'refresh_token must be a string', + }), + }), }); export async function handleRefreshToken( @@ -60,13 +70,19 @@ export async function handleRefreshToken( }); if (!refreshToken) { - throw new Error('Refresh token not found'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'Refresh token not found', + }); } if (refreshToken.expiresAt < new Date()) { // Deactivate expired token archiveRefreshToken(token); - throw new Error('Refresh token expired'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'Refresh token expired', + }); } if (refreshToken.archivedAt) { @@ -76,7 +92,10 @@ export async function handleRefreshToken( const { user, echoApp: app, session, scope } = refreshToken; if (!session) { - throw new Error('Session not found'); + throw new OAuthError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'Session not found', + }); } const sessionId = session.id; diff --git a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/route.ts b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/route.ts index ceddb8630..1aefdfc25 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/route.ts +++ b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/route.ts @@ -2,25 +2,48 @@ import { NextResponse } from 'next/server'; import z from 'zod'; -import { createZodRoute } from '@/lib/api/create-route'; - import { handleRefreshToken, handleRefreshTokenSchema } from './_lib/refresh'; import { handleIssueToken, handleIssueTokenSchema } from './_lib/issue'; -import { logger } from '@/logger'; +import { + oauthRoute, + OAuthRouteError, + oauthValidationError, +} from '../../../_lib/oauth-route'; import type { TokenMetadata } from './_lib/types'; +import { OAuthError, OAuthErrorType } from '../../../_lib/oauth-error'; + +const invalidGrantError = oauthValidationError({ + error: OAuthErrorType.INVALID_GRANT, + error_description: + 'grant_type can only be authorization_code or refresh_token', +}); -const bodySchema = z.discriminatedUnion('grant_type', [ - handleIssueTokenSchema.extend({ - grant_type: z.literal('authorization_code'), - }), - handleRefreshTokenSchema.extend({ - grant_type: z.literal('refresh_token'), - }), -]); +const bodySchema = z.discriminatedUnion( + 'grant_type', + [ + handleIssueTokenSchema.extend({ + grant_type: z.literal('authorization_code', { + error: invalidGrantError, + }), + }), + handleRefreshTokenSchema.extend({ + grant_type: z.literal('refresh_token', { + error: invalidGrantError, + }), + }), + ], + { + error: oauthValidationError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: + 'Must provide a valid grant_type (authorization_code or refresh_token)', + }), + } +); -export const POST = createZodRoute() +export const POST = oauthRoute .body(bodySchema) .handler(async (req, { body }) => { const metadata: TokenMetadata = { @@ -37,40 +60,27 @@ export const POST = createZodRoute() undefined, }; - switch (body.grant_type) { - case 'authorization_code': { - try { + try { + switch (body.grant_type) { + case 'authorization_code': { const result = await handleIssueToken(body, metadata); return NextResponse.json(result); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Unknown error'; - logger.emit({ - severityText: 'WARN', - body: 'Failed to issue OAuth token', - attributes: { - error: message, - }, - }); - return NextResponse.json({ message }, { status: 400 }); } - } - case 'refresh_token': { - try { + case 'refresh_token': { const result = await handleRefreshToken(body, metadata); return NextResponse.json(result); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Unknown error'; - logger.emit({ - severityText: 'WARN', - body: 'Failed to refresh OAuth token', - attributes: { - error: message, - }, - }); - return NextResponse.json({ message }, { status: 400 }); } } + } catch (error) { + console.log('refresh token error', error); + if (error instanceof OAuthError) { + return OAuthRouteError(error.body); + } + + return OAuthRouteError({ + error: OAuthErrorType.SERVER_ERROR, + error_description: + error instanceof Error ? error.message : 'Unknown error', + }); } }); diff --git a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/userinfo/route.ts b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/userinfo/route.ts index 5edb67026..7cec02f3a 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/userinfo/route.ts +++ b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/userinfo/route.ts @@ -1,43 +1,50 @@ import { NextResponse } from 'next/server'; import { logger } from '@/logger'; -import { authRoute } from '@/lib/api/auth-route'; +import { authMiddleware } from '@/lib/api/auth-route'; import { getFullUser } from '@/services/user/get'; import { getUnixTime } from 'date-fns'; +import { oauthRoute, OAuthRouteError } from '../../../_lib/oauth-route'; +import { OAuthErrorType } from '../../../_lib/oauth-error'; -export const GET = authRoute.handler(async (_, { ctx: { userId } }) => { - const user = await getFullUser(userId); +export const GET = oauthRoute + .use(authMiddleware) + .handler(async (_, { ctx: { userId } }) => { + const user = await getFullUser(userId); - if (!user) { - logger.emit({ - severityText: 'WARN', - body: 'User not found for valid access token in OAuth userinfo endpoint', - attributes: { - userId, - }, - }); - return NextResponse.json({ message: 'User not found' }, { status: 401 }); - } + if (!user) { + logger.emit({ + severityText: 'WARN', + body: 'User not found for valid access token in OAuth userinfo endpoint', + attributes: { + userId, + }, + }); + return OAuthRouteError({ + error: OAuthErrorType.INVALID_REQUEST, + error_description: 'User not found', + }); + } - return NextResponse.json( - { - sub: user.id, - email: user.email, - email_verified: user.emailVerified, - name: user.name || user.email, - preferred_username: user.name || user.email, - given_name: user.name?.split(' ')[0] || '', - family_name: user.name?.split(' ').slice(1).join(' ') || '', - updated_at: getUnixTime(user.updatedAt), - }, - { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - Pragma: 'no-cache', + return NextResponse.json( + { + sub: user.id, + email: user.email, + email_verified: true, + name: user.name || user.email, + preferred_username: user.name || user.email, + given_name: user.name?.split(' ')[0] || '', + family_name: user.name?.split(' ').slice(1).join(' ') || '', + updated_at: getUnixTime(user.updatedAt), }, - } - ); -}); + { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + Pragma: 'no-cache', + }, + } + ); + }); // POST is also allowed for OIDC UserInfo endpoint export const POST = GET; diff --git a/packages/app/control/src/app/api/v1/balance/[id]/free/route.ts b/packages/app/control/src/app/api/v1/balance/[id]/free/route.ts index bbdbc9a1c..8ed1f7b61 100644 --- a/packages/app/control/src/app/api/v1/balance/[id]/free/route.ts +++ b/packages/app/control/src/app/api/v1/balance/[id]/free/route.ts @@ -5,17 +5,17 @@ import { appIdSchema } from '@/services/apps/lib/schemas'; import { z } from 'zod'; const getFreeBalanceParamsSchema = z.object({ - echoAppId: appIdSchema, + id: appIdSchema, }); export const GET = authRoute .params(getFreeBalanceParamsSchema) .handler(async (_, context) => { - const { echoAppId } = context.params; + const { id } = context.params; const spendPoolInfo = await getCustomerSpendInfoForApp( context.ctx.userId, - echoAppId + id ); return NextResponse.json(spendPoolInfo); diff --git a/packages/app/control/src/app/api/v1/user/route.ts b/packages/app/control/src/app/api/v1/user/route.ts index 05999fee2..bd217217f 100644 --- a/packages/app/control/src/app/api/v1/user/route.ts +++ b/packages/app/control/src/app/api/v1/user/route.ts @@ -4,5 +4,8 @@ import { getFullUser } from '@/services/user/get'; export const GET = authRoute.handler(async (_, context) => { const user = await getFullUser(context.ctx.userId); + if (!user) { + return NextResponse.json({ message: 'User not found' }, { status: 404 }); + } return NextResponse.json(user); }); diff --git a/packages/app/control/src/env.ts b/packages/app/control/src/env.ts index 7195de2fe..9369d925d 100644 --- a/packages/app/control/src/env.ts +++ b/packages/app/control/src/env.ts @@ -4,11 +4,13 @@ import { z } from 'zod'; const IS_VERCEL_PRODUCTION = ['production', 'staging'].includes( process.env.VERCEL_ENV ?? '' ); -const IS_INTEGRATION_TEST = process.env.INTEGRATION_TEST_MODE === 'true'; +const IS_INTEGRATION_TEST = process.env.INTEGRATION_TEST_MODE === 'true'; +const SKIP_ENV_VALIDATION = Boolean(process.env.SKIP_ENV_VALIDATION); const IS_STRICT = IS_VERCEL_PRODUCTION && !IS_INTEGRATION_TEST; export const env = createEnv({ + skipValidation: SKIP_ENV_VALIDATION, /* * Serverside Environment variables, not available on the client. * Will throw if you access these variables on the client. diff --git a/packages/app/control/src/lib/api/auth-route.ts b/packages/app/control/src/lib/api/auth-route.ts index 024e879a8..957107f77 100644 --- a/packages/app/control/src/lib/api/auth-route.ts +++ b/packages/app/control/src/lib/api/auth-route.ts @@ -14,7 +14,7 @@ type MiddlewareContext = { }; // Create a middleware that checks permissions -const authMiddleware: MiddlewareFunction< +export const authMiddleware: MiddlewareFunction< Record, MiddlewareContext > = async ({ next, request }) => { diff --git a/packages/app/control/src/lib/api/create-route.ts b/packages/app/control/src/lib/api/create-route.ts index 9e29e7302..e14f3d0f3 100644 --- a/packages/app/control/src/lib/api/create-route.ts +++ b/packages/app/control/src/lib/api/create-route.ts @@ -1,11 +1,21 @@ import { RouteHandlerBuilder } from './handler-builder'; -import { HandlerServerErrorFn } from './types'; +import { + HandleInternalErrorFn, + HandlerServerErrorFn, + InternalErrorBody, + ServerErrorBody, +} from './types'; -export function createZodRoute(params?: { - handleServerError?: HandlerServerErrorFn; +export function createZodRoute< + TServerErrorBody = ServerErrorBody, + TInternalErrorBody = InternalErrorBody, +>(params?: { + handleServerError?: HandlerServerErrorFn; + handleInternalError?: HandleInternalErrorFn; }) { return new RouteHandlerBuilder({ handleServerError: params?.handleServerError, + handleInternalError: params?.handleInternalError, contextType: {}, }); } diff --git a/packages/app/control/src/lib/api/handler-builder.ts b/packages/app/control/src/lib/api/handler-builder.ts index dae20091e..9f70e4486 100644 --- a/packages/app/control/src/lib/api/handler-builder.ts +++ b/packages/app/control/src/lib/api/handler-builder.ts @@ -1,9 +1,11 @@ import z from 'zod'; import { + HandleInternalErrorFn, HandlerFunction, HandlerServerErrorFn, InternalErrorBody, + InternalRouteHandlerError, MiddlewareFunction, MiddlewareResult, NextFunction, @@ -13,21 +15,14 @@ import { import { NextResponse } from 'next/server'; import { logger } from '@/logger'; -class InternalRouteHandlerError extends Error { - readonly body: InternalErrorBody; - constructor(body: InternalErrorBody) { - super(body.message); - this.name = 'InternalRouteHandlerError'; - this.body = body; - } -} - export class RouteHandlerBuilder< TParams extends z.Schema = z.Schema, TQuery extends z.Schema = z.Schema, TBody extends z.Schema = z.Schema, TContext = object, TMetadata extends z.Schema = z.Schema, + TServerErrorBody = ServerErrorBody, + TInternalErrorBody = InternalErrorBody, > { readonly config: { paramsSchema: TParams; @@ -38,7 +33,8 @@ export class RouteHandlerBuilder< readonly middlewares: Array< MiddlewareFunction, z.infer> >; - readonly handleServerError?: HandlerServerErrorFn; + readonly handleInternalError?: HandleInternalErrorFn; + readonly handleServerError?: HandlerServerErrorFn; readonly metadataValue: z.infer | undefined; readonly contextType!: TContext; @@ -51,6 +47,7 @@ export class RouteHandlerBuilder< }, middlewares = [], handleServerError, + handleInternalError, contextType, metadataValue, }: { @@ -63,13 +60,15 @@ export class RouteHandlerBuilder< middlewares?: Array< MiddlewareFunction, z.infer> >; - handleServerError?: HandlerServerErrorFn; + handleServerError?: HandlerServerErrorFn; + handleInternalError?: HandleInternalErrorFn; contextType: TContext; metadataValue?: z.infer; }) { this.config = config; this.middlewares = middlewares; this.handleServerError = handleServerError; + this.handleInternalError = handleInternalError; this.contextType = contextType as TContext; this.metadataValue = metadataValue; } @@ -80,7 +79,15 @@ export class RouteHandlerBuilder< * @returns A new instance of the RouteHandlerBuilder */ params(schema: T) { - return new RouteHandlerBuilder({ + return new RouteHandlerBuilder< + T, + TQuery, + TBody, + TContext, + TMetadata, + TServerErrorBody, + TInternalErrorBody + >({ ...this, config: { ...this.config, paramsSchema: schema }, }); @@ -92,7 +99,15 @@ export class RouteHandlerBuilder< * @returns A new instance of the RouteHandlerBuilder */ query(schema: T) { - return new RouteHandlerBuilder({ + return new RouteHandlerBuilder< + TParams, + T, + TBody, + TContext, + TMetadata, + TServerErrorBody, + TInternalErrorBody + >({ ...this, config: { ...this.config, querySchema: schema }, }); @@ -104,7 +119,15 @@ export class RouteHandlerBuilder< * @returns A new instance of the RouteHandlerBuilder */ body(schema: T) { - return new RouteHandlerBuilder({ + return new RouteHandlerBuilder< + TParams, + TQuery, + T, + TContext, + TMetadata, + TServerErrorBody, + TInternalErrorBody + >({ ...this, config: { ...this.config, bodySchema: schema }, }); @@ -116,7 +139,15 @@ export class RouteHandlerBuilder< * @returns A new instance of the RouteHandlerBuilder */ defineMetadata(schema: T) { - return new RouteHandlerBuilder({ + return new RouteHandlerBuilder< + TParams, + TQuery, + TBody, + TContext, + T, + TServerErrorBody, + TInternalErrorBody + >({ ...this, config: { ...this.config, metadataSchema: schema }, middlewares: [], @@ -130,12 +161,18 @@ export class RouteHandlerBuilder< * @returns A new instance of the RouteHandlerBuilder */ metadata(value: z.infer) { - return new RouteHandlerBuilder( - { - ...this, - metadataValue: value, - } - ); + return new RouteHandlerBuilder< + TParams, + TQuery, + TBody, + TContext, + TMetadata, + TServerErrorBody, + TInternalErrorBody + >({ + ...this, + metadataValue: value, + }); } /** @@ -149,13 +186,7 @@ export class RouteHandlerBuilder< TNestContext & TContext, z.infer > - ): RouteHandlerBuilder< - TParams, - TQuery, - TBody, - TContext & TNestContext, - TMetadata - > { + ) { type MergedContext = TContext & TNestContext; return new RouteHandlerBuilder< @@ -163,7 +194,9 @@ export class RouteHandlerBuilder< TQuery, TBody, MergedContext, - TMetadata + TMetadata, + TServerErrorBody, + TInternalErrorBody >({ ...this, middlewares: [...this.middlewares, middleware], @@ -183,9 +216,17 @@ export class RouteHandlerBuilder< z.infer, TContext, z.infer, - TResponseBody + TResponseBody, + TServerErrorBody > - ): OriginalRouteHandler { + ): OriginalRouteHandler< + TParams, + TQuery, + TBody, + TResponseBody, + TServerErrorBody, + TInternalErrorBody + > { return async (request, context) => { try { let params = context?.params @@ -212,10 +253,18 @@ export class RouteHandlerBuilder< ) { const formData = await request.formData(); body = Object.fromEntries(formData.entries()); - } else { + } else if (contentType.includes('application/json')) { body = await request.json(); + } else { + throw new InternalRouteHandlerError({ + message: 'Invalid content-type', + errors: [], + }); } } catch (error) { + if (error instanceof InternalRouteHandlerError) { + throw error; + } if (this.config.bodySchema) { throw new InternalRouteHandlerError({ message: 'Invalid body', @@ -277,9 +326,7 @@ export class RouteHandlerBuilder< // Execute middleware chain let middlewareContext: TContext = {} as TContext; - const executeMiddlewareChain = async ( - index: number - ): Promise> => { + const executeMiddlewareChain = async (index: number) => { if (index >= this.middlewares.length) { try { const result = await handler(request, { @@ -324,22 +371,34 @@ export class RouteHandlerBuilder< headers: { 'Content-Type': 'application/json' }, }); } catch (error) { - return handleError(error as Error, this.handleServerError); + return handleError( + error as Error, + this.handleServerError, + this.handleInternalError + ); } }; return executeMiddlewareChain(0); } catch (error) { - return handleError(error as Error, this.handleServerError); + return handleError( + error as Error, + this.handleServerError, + this.handleInternalError + ); } }; } } -const handleError = ( +const handleError = < + TServerErrorBody = ServerErrorBody, + TInternalErrorBody = InternalErrorBody, +>( error: Error, - handleServerError?: HandlerServerErrorFn -): NextResponse => { + handleServerError?: HandlerServerErrorFn, + handleInternalError?: HandleInternalErrorFn +): NextResponse => { if (error instanceof InternalRouteHandlerError) { logger.emit({ severityText: 'WARN', @@ -350,6 +409,9 @@ const handleError = ( errorType: 'InternalRouteHandlerError', }, }); + if (handleInternalError) { + return handleInternalError(error); + } return NextResponse.json(error.body, { status: 400 }); } diff --git a/packages/app/control/src/lib/api/types.ts b/packages/app/control/src/lib/api/types.ts index e834fcbf2..2e29b87d1 100644 --- a/packages/app/control/src/lib/api/types.ts +++ b/packages/app/control/src/lib/api/types.ts @@ -14,6 +14,7 @@ export type HandlerFunction< TContext, TMetadata = unknown, TResponseBody = unknown, + TServerErrorBody = ServerErrorBody, > = ( request: NextRequest, context: { @@ -24,8 +25,8 @@ export type HandlerFunction< metadata?: TMetadata; } ) => - | NextResponse - | Promise>; + | NextResponse + | Promise>; /** * Function signature for the next() function in middleware @@ -83,19 +84,38 @@ export type OriginalRouteHandler< /* eslint-disable @typescript-eslint/no-unused-vars */ TBody extends z.Schema, TResponseBody, + TServerErrorBody = ServerErrorBody, + TInternalErrorBody = InternalErrorBody, > = ( request: NextRequest, context: { params: Promise> } -) => Promise>; +) => Promise< + NextResponse< + TResponseBody | TServerErrorBody | TInternalErrorBody | InternalErrorBody + > +>; + +export class InternalRouteHandlerError extends Error { + readonly body: InternalErrorBody; + constructor(body: InternalErrorBody) { + super(body.message); + this.name = 'InternalRouteHandlerError'; + this.body = body; + } +} + +export type HandleInternalErrorFn = ( + error: InternalRouteHandlerError +) => NextResponse; /** * Function that handles server errors in route handlers * @param error - The error that was thrown * @returns Response object with appropriate error details and status code */ -export type HandlerServerErrorFn = ( +export type HandlerServerErrorFn = ( error: Error -) => NextResponse; +) => NextResponse; export type ServerErrorBody = { message: string; diff --git a/packages/app/control/src/services/user/get.ts b/packages/app/control/src/services/user/get.ts index 889283641..1e8045a55 100644 --- a/packages/app/control/src/services/user/get.ts +++ b/packages/app/control/src/services/user/get.ts @@ -19,11 +19,8 @@ export const getFullUser = async (userId: string) => { id: true, name: true, image: true, - createdAt: true, - totalPaid: true, - totalSpent: true, email: true, - emailVerified: true, + createdAt: true, updatedAt: true, }, }); diff --git a/packages/app/server/eslint.config.mjs b/packages/app/server/eslint.config.mjs index bfc4e53a9..3b37e3a3f 100644 --- a/packages/app/server/eslint.config.mjs +++ b/packages/app/server/eslint.config.mjs @@ -10,8 +10,8 @@ export default [ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json', - tsconfigRootDir: '.', + project: true, + tsconfigRootDir: __dirname, }, globals: { console: 'readonly', @@ -43,7 +43,11 @@ export default [ }, { name: 'echo-server/tests', - files: ['**/__tests__/**/*.{ts,tsx}', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'], + files: [ + '**/__tests__/**/*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + ], languageOptions: { globals: { jest: 'readonly', @@ -67,12 +71,6 @@ export default [ }, { name: 'echo-server/ignores', - ignores: [ - 'node_modules/', - 'dist/', - 'build/', - 'coverage/', - '**/*.d.ts', - ], + ignores: ['node_modules/', 'dist/', 'build/', 'coverage/', '**/*.d.ts'], }, -]; \ No newline at end of file +]; diff --git a/packages/app/server/src/services/HandleStreamService.ts b/packages/app/server/src/services/HandleStreamService.ts index 9c7b969b7..c28246c85 100644 --- a/packages/app/server/src/services/HandleStreamService.ts +++ b/packages/app/server/src/services/HandleStreamService.ts @@ -44,7 +44,10 @@ export class HandleStreamService { // Promise for processing data and creating transaction const reader2 = accountingStream.getReader(); - const transactionPromise = this.processStreamData(reader2, provider); + const transactionPromise = this.processStreamData( + reader2 as ReadableStreamDefaultReader, + provider + ); // Wait for both streams to complete before ending response try { diff --git a/packages/app/server/src/services/ModelRequestService.ts b/packages/app/server/src/services/ModelRequestService.ts index 20068aa65..97267d09c 100644 --- a/packages/app/server/src/services/ModelRequestService.ts +++ b/packages/app/server/src/services/ModelRequestService.ts @@ -144,7 +144,9 @@ export class ModelRequestService { // Add files from req.files if (req.files && Array.isArray(req.files)) { req.files.forEach(file => { - const blob = new Blob([file.buffer], { type: file.mimetype }); + const blob = new Blob([new Uint8Array(file.buffer)], { + type: file.mimetype, + }); formData.append(file.fieldname, blob, file.originalname); }); } diff --git a/packages/sdk/component-registry/.env.local b/packages/sdk/component-registry/.env.local new file mode 100644 index 000000000..57ef8b862 --- /dev/null +++ b/packages/sdk/component-registry/.env.local @@ -0,0 +1 @@ +NEXT_PUBLIC_ECHO_APP_ID="6f0226b3-9d95-4d5a-96de-d178bd4dc9f7" \ No newline at end of file diff --git a/packages/sdk/component-registry/.gitignore b/packages/sdk/component-registry/.gitignore index 5ef6a5207..e72b4d6a4 100644 --- a/packages/sdk/component-registry/.gitignore +++ b/packages/sdk/component-registry/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env # vercel .vercel diff --git a/packages/sdk/component-registry/next.config.ts b/packages/sdk/component-registry/next.config.ts index e9ffa3083..5e891cf00 100644 --- a/packages/sdk/component-registry/next.config.ts +++ b/packages/sdk/component-registry/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ diff --git a/packages/sdk/component-registry/package.json b/packages/sdk/component-registry/package.json index 71b783f11..4caf63230 100644 --- a/packages/sdk/component-registry/package.json +++ b/packages/sdk/component-registry/package.json @@ -10,9 +10,9 @@ "lint": "next lint" }, "dependencies": { - "@merit-systems/echo-next-sdk": "^0.0.20", - "@merit-systems/echo-react-sdk": "^1.0.29", - "@merit-systems/echo-typescript-sdk": "^1.0.14", + "@merit-systems/echo-next-sdk": "workspace:*", + "@merit-systems/echo-react-sdk": "workspace:*", + "@merit-systems/echo-typescript-sdk": "workspace:*", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", @@ -24,7 +24,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.487.0", - "next": "15.5.2", + "next": "15.1.4", "next-themes": "^0.4.6", "react": "19.1.0", "react-dom": "19.1.0", @@ -40,14 +40,8 @@ "@types/react": "19.1.2", "@types/react-dom": "19.1.2", "eslint": "^9.32.0", - "eslint-config-next": "15.3.1", + "eslint-config-next": "^15.1.4", "tailwindcss": "^4.1.11", "typescript": "^5.9.2" - }, - "pnpm": { - "overrides": { - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2" - } } } diff --git a/packages/sdk/component-registry/public/r/echo-account-next.json b/packages/sdk/component-registry/public/r/echo-account-next.json index d2b8e436d..de04ea833 100644 --- a/packages/sdk/component-registry/public/r/echo-account-next.json +++ b/packages/sdk/component-registry/public/r/echo-account-next.json @@ -4,11 +4,7 @@ "type": "registry:component", "title": "Echo Account (Next)", "description": "Next.js variant of Echo Account", - "registryDependencies": [ - "popover", - "skeleton", - "avatar" - ], + "registryDependencies": ["popover", "skeleton", "avatar"], "files": [ { "path": "registry/echo/blocks/echo-account-button/echo-account-next.tsx", @@ -68,4 +64,4 @@ "target": "~/public/logo/dark.svg" } ] -} \ No newline at end of file +} diff --git a/packages/sdk/component-registry/public/r/echo-account-react.json b/packages/sdk/component-registry/public/r/echo-account-react.json index ca2877a89..1f9d6b168 100644 --- a/packages/sdk/component-registry/public/r/echo-account-react.json +++ b/packages/sdk/component-registry/public/r/echo-account-react.json @@ -4,11 +4,7 @@ "type": "registry:component", "title": "Echo Account (React)", "description": "React variant of Echo Account", - "registryDependencies": [ - "popover", - "skeleton", - "avatar" - ], + "registryDependencies": ["popover", "skeleton", "avatar"], "files": [ { "path": "registry/echo/blocks/echo-account-button/echo-account-react.tsx", @@ -68,4 +64,4 @@ "target": "~/public/logo/dark.svg" } ] -} \ No newline at end of file +} diff --git a/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-account-react.tsx b/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-account-react.tsx index 5c907fb72..cf4456550 100644 --- a/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-account-react.tsx +++ b/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-account-react.tsx @@ -1,5 +1,5 @@ import { useEcho } from '@merit-systems/echo-react-sdk'; -import { EchoAccountButton } from './echo-account'; +import { EchoAccountButton } from './echo-account'; export function EchoAccount() { const echo = useEcho(); diff --git a/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-popover.tsx b/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-popover.tsx index a705cbc62..80d1ad7dd 100644 --- a/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-popover.tsx +++ b/packages/sdk/component-registry/registry/echo/blocks/echo-account-button/echo-popover.tsx @@ -22,7 +22,7 @@ export function EchoAccountButtonPopover({ echo }: { echo: EchoContextValue }) { >
- + {user?.name?.charAt(0) || user?.email?.charAt(0)} diff --git a/packages/sdk/next/package.json b/packages/sdk/next/package.json index 7388476de..5846bac20 100644 --- a/packages/sdk/next/package.json +++ b/packages/sdk/next/package.json @@ -26,7 +26,9 @@ "test:unit": "echo 'no tests'", "test:watch": "echo 'no tests'", "prepublishOnly": "pnpm run build", - "prepare": "tsup src/index.ts src/client.ts --dts --format esm --out-dir dist" + "prepare": "tsup src/index.ts src/client.ts --dts --format esm --out-dir dist", + "publish:test": "pnpm publish --access public --dry-run", + "publish:real": "pnpm publish --access public" }, "keywords": [ "echo", diff --git a/packages/sdk/next/src/auth/oauth-handlers.ts b/packages/sdk/next/src/auth/oauth-handlers.ts index ac35e1eea..4daf91e4e 100644 --- a/packages/sdk/next/src/auth/oauth-handlers.ts +++ b/packages/sdk/next/src/auth/oauth-handlers.ts @@ -1,9 +1,12 @@ -import { EchoConfig } from '@merit-systems/echo-typescript-sdk'; +import { + EchoConfig, + CreateOauthTokenResponse, +} from '@merit-systems/echo-typescript-sdk'; import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { resolveEchoBaseUrl } from '../config'; import { ECHO_COOKIE, namespacedCookie } from './cookie-names'; -import { getEchoToken, RefreshTokenResponse } from './token-manager'; +import { getEchoToken } from './token-manager'; /** * Generate PKCE code challenge and verifier @@ -174,7 +177,7 @@ export async function handleCallback( ); } - const tokenData = (await tokenResponse.json()) as RefreshTokenResponse; + const tokenData = (await tokenResponse.json()) as CreateOauthTokenResponse; const response = NextResponse.redirect(`${origin}`); diff --git a/packages/sdk/next/src/auth/token-manager.ts b/packages/sdk/next/src/auth/token-manager.ts index f6f4e4005..e64cf1ae8 100644 --- a/packages/sdk/next/src/auth/token-manager.ts +++ b/packages/sdk/next/src/auth/token-manager.ts @@ -1,32 +1,13 @@ import { EchoClient, EchoConfig, - User, + CreateOauthTokenResponse, } from '@merit-systems/echo-typescript-sdk'; import { cookies as getCookies } from 'next/headers'; import { resolveEchoBaseUrl } from '../config'; import { ECHO_COOKIE, namespacedCookie } from './cookie-names'; import { shouldRefreshToken } from './jwt-utils'; -export interface RefreshTokenResponse { - access_token: string; - token_type: string; - expires_in: number; - refresh_token: string; - refresh_token_expires_in: number; - scope: string; - user: { - id: string; - email: string; - name: string; - }; - echo_app: { - id: string; - name: string; - description: string; - }; -} - /** * Refresh an access token using a refresh token * @param refreshToken - The refresh token to use @@ -36,7 +17,7 @@ export interface RefreshTokenResponse { export async function performTokenRefresh( refreshToken: string, config: EchoConfig -): Promise { +): Promise { return fetch(`${resolveEchoBaseUrl(config)}/api/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -47,7 +28,7 @@ export async function performTokenRefresh( }), }).then(async r => { if (r.ok) { - const tokenData = (await r.json()) as RefreshTokenResponse; + const tokenData = (await r.json()) as CreateOauthTokenResponse; return tokenData; } return Promise.reject(new Error(await r.text())); @@ -129,7 +110,7 @@ export async function getEchoToken(config: EchoConfig): Promise { return accessToken; } -export async function getUser(config: EchoConfig): Promise { +export async function getUser(config: EchoConfig) { const echo = await getEchoClient(config); if (!echo) { throw new Error('User not signed in'); diff --git a/packages/sdk/next/src/index.ts b/packages/sdk/next/src/index.ts index 25c7babaf..17652fb11 100644 --- a/packages/sdk/next/src/index.ts +++ b/packages/sdk/next/src/index.ts @@ -7,11 +7,10 @@ import { createEchoAnthropic } from 'ai-providers/anthropic'; import { createEchoGoogle } from 'ai-providers/google'; import { createEchoOpenAI } from 'ai-providers/openai'; +import { CreateOauthTokenResponse } from '@merit-systems/echo-typescript-sdk'; + import { ECHO_COOKIE, namespacedCookie } from 'auth/cookie-names'; -import { - RefreshTokenResponse, - getEchoToken as getEchoTokenInternal, -} from 'auth/token-manager'; +import { getEchoToken as getEchoTokenInternal } from 'auth/token-manager'; import { handleEchoClientProxy } from 'proxy'; import { handleCallback, @@ -73,7 +72,7 @@ export default function Echo(config: EchoConfig): EchoResult { if (!userInfo) { return null; } - const user = JSON.parse(userInfo) as RefreshTokenResponse['user']; + const user = JSON.parse(userInfo) as CreateOauthTokenResponse['user']; return user; }; diff --git a/packages/sdk/next/src/types.ts b/packages/sdk/next/src/types.ts index 105d27c9d..815198059 100644 --- a/packages/sdk/next/src/types.ts +++ b/packages/sdk/next/src/types.ts @@ -2,9 +2,8 @@ import { AnthropicProvider, GoogleGenerativeAIProvider, OpenAIProvider, - User, + CreateOauthTokenResponse, } from '@merit-systems/echo-typescript-sdk'; -import { RefreshTokenResponse } from 'auth/token-manager'; import { NextRequest } from 'next/server'; export interface EchoConfig { @@ -38,7 +37,7 @@ export type AppRouteHandlers = Record< export type EchoResult = { handlers: AppRouteHandlers; - getUser: () => Promise; + getUser: () => Promise; isSignedIn: () => Promise; /** Get the Echo access token for server-side API calls */ diff --git a/packages/sdk/react/package.json b/packages/sdk/react/package.json index 6848c4938..a03d952ab 100644 --- a/packages/sdk/react/package.json +++ b/packages/sdk/react/package.json @@ -30,8 +30,8 @@ "test:coverage": "vitest run --coverage", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "publish:test": "npm publish --access public --dry-run", - "publish:real": "npm publish --access public", + "publish:test": "pnpm publish --access public --dry-run", + "publish:real": "pnpm publish --access public", "prepare": "sh -c 'if [ \"$CI\" = true ]; then echo \"Skipping prepare in CI\"; else tsup src/index.ts --dts --format esm,cjs --out-dir dist; fi'", "prepublishOnly": "pnpm run build" }, diff --git a/packages/sdk/react/src/__tests__/security/component-security.test.tsx b/packages/sdk/react/src/__tests__/security/component-security.test.tsx index f537d1f4e..c9b7829bf 100644 --- a/packages/sdk/react/src/__tests__/security/component-security.test.tsx +++ b/packages/sdk/react/src/__tests__/security/component-security.test.tsx @@ -304,7 +304,7 @@ describe('Component Security Integration', () => {
{user?.name}
{user?.email}
-
{user?.picture}
+
{user?.image}
{user?.id}
); diff --git a/packages/sdk/react/src/components/EchoTokens.tsx b/packages/sdk/react/src/components/EchoTokens.tsx index e548284ed..a460fd46a 100644 --- a/packages/sdk/react/src/components/EchoTokens.tsx +++ b/packages/sdk/react/src/components/EchoTokens.tsx @@ -144,7 +144,6 @@ export function EchoTokens({ balance, freeTierBalance, isLoading, - isLoggedIn, refreshBalance, isInsufficientFunds, setIsInsufficientFunds, @@ -320,9 +319,9 @@ export function EchoTokens({ onMouseLeave={() => setIsHovered(false)} > {/* Avatar or Logo */} - {showAvatar && user?.picture ? ( + {showAvatar && user?.image ? ( {user.name Promise; diff --git a/packages/sdk/react/src/hooks/useEchoBalance.ts b/packages/sdk/react/src/hooks/useEchoBalance.ts index 31d97d926..83c7d154a 100644 --- a/packages/sdk/react/src/hooks/useEchoBalance.ts +++ b/packages/sdk/react/src/hooks/useEchoBalance.ts @@ -1,7 +1,6 @@ -import type { FreeBalance } from '@merit-systems/echo-typescript-sdk'; -import { EchoClient, parseEchoError } from '@merit-systems/echo-typescript-sdk'; import useSWR from 'swr'; -import { EchoBalance } from '../types'; + +import { EchoClient, parseEchoError } from '@merit-systems/echo-typescript-sdk'; export function useEchoBalance(echoClient: EchoClient | null, appId: string) { const { data, error, isLoading, mutate } = useSWR( diff --git a/packages/sdk/react/src/hooks/useEchoUser.ts b/packages/sdk/react/src/hooks/useEchoUser.ts index 160e49b7a..e315ab6c3 100644 --- a/packages/sdk/react/src/hooks/useEchoUser.ts +++ b/packages/sdk/react/src/hooks/useEchoUser.ts @@ -1,6 +1,5 @@ import { EchoClient, parseEchoError } from '@merit-systems/echo-typescript-sdk'; import useSWR from 'swr'; -import { EchoUser } from '../types'; export function useEchoUser(echoClient: EchoClient | null) { const { diff --git a/packages/sdk/react/src/stories/EchoTokens.stories.tsx b/packages/sdk/react/src/stories/EchoTokens.stories.tsx index ed2be7534..3b47b5bc7 100644 --- a/packages/sdk/react/src/stories/EchoTokens.stories.tsx +++ b/packages/sdk/react/src/stories/EchoTokens.stories.tsx @@ -127,7 +127,12 @@ export const ZeroFreeTierWithRealBalance: Story = { mockState={{ isLoggedIn: true, user: mockStates.authenticated.user, - balance: { totalPaid: 2.5, totalSpent: 0.5, balance: 2.0 }, + balance: { + totalPaid: 2.5, + totalSpent: 0.5, + balance: 2.0, + currency: 'USD', + }, freeTierBalance: { spendPoolBalance: 0, userSpendInfo: { diff --git a/packages/sdk/react/src/stories/MockEchoProvider.tsx b/packages/sdk/react/src/stories/MockEchoProvider.tsx index 90abe6d3f..bab49677f 100644 --- a/packages/sdk/react/src/stories/MockEchoProvider.tsx +++ b/packages/sdk/react/src/stories/MockEchoProvider.tsx @@ -1,4 +1,4 @@ -import { FreeBalance } from '@merit-systems/echo-typescript-sdk'; +import { GetBalanceByIdFreeResponse } from '@merit-systems/echo-typescript-sdk'; import { User } from 'oidc-client-ts'; import { ReactNode } from 'react'; import { EchoContext, EchoContextValue } from '../context'; @@ -19,7 +19,7 @@ const mockUser: EchoUser = { id: 'mock-user-123', email: 'demo@echo-systems.com', name: 'Demo User', - picture: 'https://avatars.githubusercontent.com/u/11855252?v=4', + image: 'https://avatars.githubusercontent.com/u/11855252?v=4', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', }; @@ -28,9 +28,10 @@ const mockBalance: EchoBalance = { totalPaid: 150, totalSpent: 0, balance: 150, + currency: 'USD', }; -const mockFreeTierBalance: FreeBalance = { +const mockFreeTierBalance: GetBalanceByIdFreeResponse = { spendPoolBalance: 100, userSpendInfo: { userId: 'mock-user-123', @@ -133,14 +134,14 @@ export const mockStates = { lowBalance: createMockContext({ isLoggedIn: true, user: mockUser as EchoUser, - balance: { totalPaid: 5, totalSpent: 0, balance: 5 }, + balance: { totalPaid: 5, totalSpent: 0, balance: 5, currency: 'USD' }, isLoading: false, }), freeTierOnly: createMockContext({ isLoggedIn: true, user: mockUser as EchoUser, - balance: { totalPaid: 0, totalSpent: 0, balance: 0 }, + balance: { totalPaid: 0, totalSpent: 0, balance: 0, currency: 'USD' }, freeTierBalance: { spendPoolBalance: 50, userSpendInfo: { diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index abf9f02ef..f4f4189ae 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -29,7 +29,9 @@ "update-models:gemini": "tsx scripts/update-gemini-models.ts", "update-models:openrouter": "tsx scripts/update-openrouter-models.ts", "update-all-models": "pnpm run update-models:openai && pnpm run update-models:anthropic && pnpm run update-models:gemini && pnpm run update-models:openrouter", - "prepublishOnly": "pnpm run build" + "prepublishOnly": "pnpm run build", + "publish:test": "pnpm publish --access public --dry-run", + "publish:real": "pnpm publish --access public" }, "keywords": [ "echo", diff --git a/packages/sdk/ts/src/api-types.ts b/packages/sdk/ts/src/api-types.ts index 29a1cc033..ebaa3b396 100644 --- a/packages/sdk/ts/src/api-types.ts +++ b/packages/sdk/ts/src/api-types.ts @@ -110,12 +110,65 @@ export type GetUserResponse = { name: string | null; createdAt: string; updatedAt: string; - totalPaid: string; - totalSpent: string; email: string; - emailVerified: string | null; image: string | null; -} | null; +}; + +export type GetOauthAuthorizeQuery = { + client_id: string; + redirect_uri: string; + code_challenge: string; + code_challenge_method: 'S256'; + scope: string; + state: string; + response_type: 'code'; + prompt?: 'none' | undefined; + new_user?: 'true' | undefined; + referral_code?: string | undefined; +}; + +export type CreateOauthTokenResponse = { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + refresh_token_expires_in: number; + scope: string; + user: { id: string; email: string; name: string }; + echo_app: { id: string; name: string; description: string }; +}; + +export type CreateOauthTokenBody = + | { + redirect_uri: string; + client_id: string; + code: string; + code_verifier: string; + grant_type: 'authorization_code'; + } + | { refresh_token: string; grant_type: 'refresh_token' }; + +export type GetOauthUserinfoResponse = { + sub: string; + email: string; + email_verified: boolean; + name: string; + preferred_username: string; + given_name: string; + family_name: string; + updated_at: number; +}; + +export type CreateOauthUserinfoResponse = { + sub: string; + email: string; + email_verified: boolean; + name: string; + preferred_username: string; + given_name: string; + family_name: string; + updated_at: number; +}; export type ApiRoutes = { 'GET /apps/{id}': { @@ -141,4 +194,11 @@ export type ApiRoutes = { body: CreateUserReferralBody; }; 'GET /user': { response: GetUserResponse }; + 'GET /oauth/authorize': { query: GetOauthAuthorizeQuery }; + 'POST /oauth/token': { + response: CreateOauthTokenResponse; + body: CreateOauthTokenBody; + }; + 'GET /oauth/userinfo': { response: GetOauthUserinfoResponse }; + 'POST /oauth/userinfo': { response: CreateOauthUserinfoResponse }; }; diff --git a/packages/sdk/ts/src/resources/balance.ts b/packages/sdk/ts/src/resources/balance.ts index 3b5c2633a..693728b7a 100644 --- a/packages/sdk/ts/src/resources/balance.ts +++ b/packages/sdk/ts/src/resources/balance.ts @@ -25,11 +25,8 @@ export class BalanceResource extends BaseResource { async getFreeBalance( echoAppId: string ): Promise { - const request: ApiRoutes['GET /balance/{id}/free']['params'] = { - echoAppId, - }; return this.handleRequest( - () => this.http.get(`/api/v1/balance/${echoAppId}/free`, request), + () => this.http.get(`/api/v1/balance/${echoAppId}/free`), 'fetching free tier balance', '/api/v1/balance/free' ); diff --git a/packages/tests/integration/tests/oauth-protocol/jwt-lifecycle.test.ts b/packages/tests/integration/tests/oauth-protocol/jwt-lifecycle.test.ts index 6f3191b0a..3a0c545db 100644 --- a/packages/tests/integration/tests/oauth-protocol/jwt-lifecycle.test.ts +++ b/packages/tests/integration/tests/oauth-protocol/jwt-lifecycle.test.ts @@ -24,7 +24,7 @@ describe('JWT Token Lifecycle Tests', () => { code_verifier: 'test-verifier-for-jwt-validation-that-meets-minimum-43-character-requirement-for-pkce', }) - ).rejects.toThrow(/invalid.*grant|authorization.*code/i); + ).rejects.toThrow(/invalid authorization code/i); }); }); diff --git a/packages/tests/integration/tests/oauth-protocol/oauth-compliance.test.ts b/packages/tests/integration/tests/oauth-protocol/oauth-compliance.test.ts index e0b0754a4..bf85a9417 100644 --- a/packages/tests/integration/tests/oauth-protocol/oauth-compliance.test.ts +++ b/packages/tests/integration/tests/oauth-protocol/oauth-compliance.test.ts @@ -49,7 +49,7 @@ describe('OAuth 2.0 Protocol Compliance', () => { code_challenge: '', code_challenge_method: 'S256', }) - ).rejects.toThrow(/code_challenge must be at least 43 characters/i); + ).rejects.toThrow(/code_challenge must be base64url encoded/i); }); test('validates client_id against database', async () => { @@ -64,7 +64,9 @@ describe('OAuth 2.0 Protocol Compliance', () => { code_challenge: codeChallenge, code_challenge_method: 'S256', }) - ).rejects.toThrow(/not_found.*Echo app not found/i); + ).rejects.toThrow( + /\{"error":"invalid_client","error_description":"Echo app not found"\}/i + ); }); test('enforces redirect URI allowlist', async () => { @@ -145,8 +147,8 @@ describe('OAuth 2.0 Protocol Compliance', () => { if (response.status === 400) { // Parameter validation rejected it (ideal case) const result = await response.json(); - expect(result.message).toBe('Invalid query'); - expect(JSON.stringify(result.errors)).toMatch( + expect(result.error).toBe('invalid_request'); + expect(result.error_description).toMatch( /Only S256 code challenge method is supported/i ); } else if (response.status === 302) { @@ -200,8 +202,8 @@ describe('OAuth 2.0 Protocol Compliance', () => { expect(response.status).toBe(400); const result = await response.json(); - expect(result.message).toBe('Invalid query'); - expect(JSON.stringify(result.errors)).toMatch( + expect(result.error).toBe('invalid_request'); + expect(result.error_description).toMatch( /Only authorization code flow \(response_type=code\) is supported/i ); } diff --git a/packages/tests/integration/tests/oauth-protocol/oauth-refresh.test.ts b/packages/tests/integration/tests/oauth-protocol/oauth-refresh.test.ts index 5d26430e4..281d72314 100644 --- a/packages/tests/integration/tests/oauth-protocol/oauth-refresh.test.ts +++ b/packages/tests/integration/tests/oauth-protocol/oauth-refresh.test.ts @@ -63,7 +63,9 @@ describe('OAuth Refresh Token Tests', () => { expect(response.ok).toBe(false); const errorData = await response.json(); - expect(errorData.error).toMatch(/unsupported.*grant.*type/i); + expect(errorData.error_description).toMatch( + /must provide a valid grant_type \(authorization_code or refresh_token\)/i + ); }); }); @@ -190,7 +192,7 @@ describe('OAuth Refresh Token Tests', () => { refresh_token: 'test-refresh-token', client_id: '99999999-9999-9999-9999-999999999999', // Non-existent }) - ).rejects.toThrow(/invalid.*grant|invalid.*client|client.*not.*found/i); + ).rejects.toThrow(/refresh token not found/i); }); }); @@ -214,8 +216,8 @@ describe('OAuth Refresh Token Tests', () => { // Should fail due to invalid token, not content-type expect(response.ok).toBe(false); const errorData = await response.json(); - expect(errorData.error).not.toMatch(/content.*type|unsupported.*media/i); - expect(errorData.error).toMatch(/invalid.*grant|invalid.*token/i); + expect(typeof errorData.error_description).toBe('string'); + expect(errorData.error_description).not.toEqual('Invalid content-type'); }); test('supports application/x-www-form-urlencoded content-type', async () => { @@ -239,10 +241,8 @@ describe('OAuth Refresh Token Tests', () => { // Should fail due to invalid token, not content-type expect(response.ok).toBe(false); const errorData = await response.json(); - expect(errorData.error).not.toMatch(/content.*type|unsupported.*media/i); - expect(errorData.error).toMatch( - /invalid.*grant|invalid.*token|server.*error/i - ); + expect(typeof errorData.error_description).toBe('string'); + expect(errorData.error_description).not.toEqual('Invalid content-type'); }); test('rejects unsupported content-types', async () => { @@ -259,9 +259,8 @@ describe('OAuth Refresh Token Tests', () => { expect(response.ok).toBe(false); const errorData = await response.json(); - expect(errorData.error).toMatch( - /invalid.*request|unsupported.*media|content.*type|server.*error/i - ); + expect(typeof errorData.error_description).toBe('string'); + expect(errorData.error_description).toBe('Invalid content-type'); }); }); diff --git a/packages/tests/integration/tests/oauth-protocol/pkce-security.test.ts b/packages/tests/integration/tests/oauth-protocol/pkce-security.test.ts index 09bf4107e..aa1c32353 100644 --- a/packages/tests/integration/tests/oauth-protocol/pkce-security.test.ts +++ b/packages/tests/integration/tests/oauth-protocol/pkce-security.test.ts @@ -63,7 +63,7 @@ describe('PKCE Security Validation', () => { redirect_uri: 'http://localhost:3000/callback', code_verifier: attackerCodeVerifier, // Wrong verifier! }) - ).rejects.toThrow(/invalid.*grant|code.*verifier.*invalid/i); + ).rejects.toThrow(/invalid authorization code/i); }); test('prevents code interception attacks', async () => { @@ -104,7 +104,7 @@ describe('PKCE Security Validation', () => { redirect_uri: 'http://localhost:3000/callback', code_verifier: generateCodeVerifier(), // Wrong verifier }) - ).rejects.toThrow(/invalid.*grant|code.*verifier.*invalid/i); + ).rejects.toThrow(/invalid authorization code/i); }); test('prevents replay attacks', async () => { @@ -136,7 +136,7 @@ describe('PKCE Security Validation', () => { } catch (error) { const err = error as Error; // Should fail with invalid_grant (because mock code), not verifier issues - expect(err.message).toMatch(/invalid.*grant/i); + expect(err.message).toMatch(/invalid authorization code/i); expect(err.message).not.toMatch(/verifier/i); } @@ -148,7 +148,7 @@ describe('PKCE Security Validation', () => { redirect_uri: 'http://localhost:3000/callback', code_verifier: codeVerifier, }) - ).rejects.toThrow(/invalid.*grant|code.*expired|already.*used/i); + ).rejects.toThrow(/invalid authorization code/i); }); }); @@ -170,12 +170,26 @@ describe('PKCE Security Validation', () => { test('prevents malformed code challenge attacks', async () => { const malformedChallenges = [ - '', // Empty - 'invalid!@#$%', // Invalid characters - 'a', // Too short - 'A'.repeat(200), // Too long - '====', // Invalid base64url - 'contains+and/characters', // Base64 instead of base64url + { + value: 'invalid!@#$%', + expectedError: /code_challenge must be base64url encoded/i, + }, // Invalid characters + { + value: 'a', + expectedError: /code_challenge must be at least 43 characters/i, + }, // Too short + { + value: 'A'.repeat(200), + expectedError: /code_challenge must be at most 128 characters/i, + }, // Too long + { + value: '====', + expectedError: /code_challenge must be base64url encoded/i, + }, // Invalid base64url + { + value: 'contains+and/characters', + expectedError: /code_challenge must be base64url encoded/i, + }, // Base64 instead of base64url ]; for (const challenge of malformedChallenges) { @@ -184,10 +198,10 @@ describe('PKCE Security Validation', () => { client_id: TEST_CLIENT_IDS.primary, redirect_uri: 'http://localhost:3000/callback', state: generateState(), - code_challenge: challenge, + code_challenge: challenge.value, code_challenge_method: 'S256', }) - ).rejects.toThrow(/invalid.*challenge|malformed.*challenge/i); + ).rejects.toThrow(challenge.expectedError); } }); @@ -345,9 +359,9 @@ describe('PKCE Security Validation', () => { } catch (error) { const err = error as Error; expect(err.message).not.toMatch( - /code.*verifier.*invalid|invalid.*characters/i + /code_verifier contains invalid characters/i ); - expect(err.message).toMatch(/invalid.*grant|authorization.*code/i); + expect(err.message).toMatch(/Invalid authorization code/i); } }); }); diff --git a/packages/tests/integration/utils/api-client.ts b/packages/tests/integration/utils/api-client.ts index 4e727586d..a07e6f7d2 100644 --- a/packages/tests/integration/utils/api-client.ts +++ b/packages/tests/integration/utils/api-client.ts @@ -1,9 +1,5 @@ import { TEST_CONFIG } from '../config/index.js'; -import { - Balance, - EchoClient, - FreeBalance, -} from '@merit-systems/echo-typescript-sdk'; +import { EchoClient } from '@merit-systems/echo-typescript-sdk'; export interface EchoControlApiClient { baseUrl: string; @@ -221,7 +217,7 @@ export class EchoControlApiClient { } // Balance endpoint (requires authentication) - async getBalance(authToken: string): Promise { + async getBalance(authToken: string) { const echoClient = new EchoClient({ apiKey: authToken, baseUrl: this.baseUrl, @@ -255,10 +251,7 @@ export class EchoControlApiClient { }; } - async getFreeTierBalance( - authToken: string, - echoAppId: string - ): Promise { + async getFreeTierBalance(authToken: string, echoAppId: string) { const echoClient = new EchoClient({ apiKey: authToken, baseUrl: this.baseUrl, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf67657bc..05eb5ea0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,9 +70,6 @@ importers: '@icons-pack/react-simple-icons': specifier: ^13.7.0 version: 13.7.0(react@19.1.1) - '@merit-systems/echo-typescript-sdk': - specifier: 1.0.14 - version: 1.0.14(zod@4.1.7) '@merit-systems/sdk': specifier: 0.0.8 version: 0.0.8 @@ -532,14 +529,14 @@ importers: packages/sdk/component-registry: dependencies: '@merit-systems/echo-next-sdk': - specifier: ^0.0.20 - version: 0.0.20(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.76) + specifier: workspace:* + version: link:../next '@merit-systems/echo-react-sdk': - specifier: ^1.0.29 - version: 1.0.29(openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.76) + specifier: workspace:* + version: link:../react '@merit-systems/echo-typescript-sdk': - specifier: ^1.0.14 - version: 1.0.14(zod@3.25.76) + specifier: workspace:* + version: link:../ts '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -574,8 +571,8 @@ importers: specifier: ^0.487.0 version: 0.487.0(react@19.1.0) next: - specifier: 15.5.2 - version: 15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.1.4 + version: 15.1.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -617,8 +614,8 @@ importers: specifier: ^9.32.0 version: 9.35.0(jiti@2.5.1) eslint-config-next: - specifier: 15.3.1 - version: 15.3.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) + specifier: ^15.1.4 + version: 15.5.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) tailwindcss: specifier: ^4.1.11 version: 4.1.13 @@ -1793,33 +1790,65 @@ packages: peerDependencies: react: ^16.13 || ^17 || ^18 || ^19 + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-arm64@0.34.3': resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.3': resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.0': resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.0': resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.0': resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + '@img/sharp-libvips-linux-arm@1.2.0': resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] @@ -1830,32 +1859,64 @@ packages: cpu: [ppc64] os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.0': resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linux-x64@1.2.0': resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.0': resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm64@0.34.3': resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + '@img/sharp-linux-arm@0.34.3': resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1868,30 +1929,59 @@ packages: cpu: [ppc64] os: [linux] + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-s390x@0.34.3': resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linux-x64@0.34.3': resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.3': resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.34.3': resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + '@img/sharp-wasm32@0.34.3': resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1903,12 +1993,24 @@ packages: cpu: [arm64] os: [win32] + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-ia32@0.34.3': resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@img/sharp-win32-x64@0.34.3': resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2029,23 +2131,6 @@ packages: '@types/react': 19.1.10 react: '>=16' - '@merit-systems/echo-next-sdk@0.0.20': - resolution: {integrity: sha512-vK8RiVIRH1BzvjmdUuxO65t/AUHEOc/9fN28DO3vvolaQpQJnH1RnpTv2Uen4kFC1vKSnvuDNVbd1+8sXtdIeg==} - peerDependencies: - next: '>=15.0.0' - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - - '@merit-systems/echo-react-sdk@1.0.29': - resolution: {integrity: sha512-VHyyvXStO6Qs1RfO+29RDZ8NG3OCcEeHpBltZhJ4Oh189KvB+2F/PzIlQPIUiYhoKJuiwQxXoQJ70OGjAb0xMw==} - peerDependencies: - openai: ^4.0.0 || ^5.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - openai: - optional: true - '@merit-systems/echo-typescript-sdk@1.0.14': resolution: {integrity: sha512-tpN6KIhWIUXJvQwYFrdwjagfU/wuefh6O/5JQ4r1/0NtDeyF9EBe7oJlpyuPxQNUAt1toiZ+f4nB+KI0HT/CSg==} @@ -2146,21 +2231,27 @@ packages: '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + '@next/env@15.1.4': + resolution: {integrity: sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==} + '@next/env@15.4.7': resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} '@next/env@15.5.2': resolution: {integrity: sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==} - '@next/eslint-plugin-next@15.3.1': - resolution: {integrity: sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==} - '@next/eslint-plugin-next@15.4.7': resolution: {integrity: sha512-asj3RRiEruRLVr+k2ZC4hll9/XBzegMpFMr8IIRpNUYypG86m/a76339X2WETl1C53A512w2INOc2KZV769KPA==} '@next/eslint-plugin-next@15.5.0': resolution: {integrity: sha512-+k83U/fST66eQBjTltX2T9qUYd43ntAe+NZ5qeZVTQyTiFiHvTLtkpLKug4AnZAtuI/lwz5tl/4QDJymjVkybg==} + '@next/swc-darwin-arm64@15.1.4': + resolution: {integrity: sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@15.4.7': resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==} engines: {node: '>= 10'} @@ -2173,6 +2264,12 @@ packages: cpu: [arm64] os: [darwin] + '@next/swc-darwin-x64@15.1.4': + resolution: {integrity: sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@15.4.7': resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==} engines: {node: '>= 10'} @@ -2185,6 +2282,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-linux-arm64-gnu@15.1.4': + resolution: {integrity: sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-gnu@15.4.7': resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==} engines: {node: '>= 10'} @@ -2197,6 +2300,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-musl@15.1.4': + resolution: {integrity: sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-musl@15.4.7': resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==} engines: {node: '>= 10'} @@ -2209,6 +2318,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-x64-gnu@15.1.4': + resolution: {integrity: sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-gnu@15.4.7': resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==} engines: {node: '>= 10'} @@ -2221,6 +2336,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-linux-x64-musl@15.1.4': + resolution: {integrity: sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-musl@15.4.7': resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==} engines: {node: '>= 10'} @@ -2233,6 +2354,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-win32-arm64-msvc@15.1.4': + resolution: {integrity: sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-arm64-msvc@15.4.7': resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==} engines: {node: '>= 10'} @@ -2245,6 +2372,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-x64-msvc@15.1.4': + resolution: {integrity: sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@15.4.7': resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==} engines: {node: '>= 10'} @@ -4003,6 +4136,9 @@ packages: typescript: optional: true + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -6031,15 +6167,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.3.1: - resolution: {integrity: sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - eslint-config-next@15.4.7: resolution: {integrity: sha512-tkKKNVJKI4zMIgTpvG2x6mmdhuOdgXUL3AaSPHwxLQkvzi4Yryqvk6B0R5Z4gkpe7FKopz3ZmlpePH3NTHy3gA==} peerDependencies: @@ -7922,6 +8049,27 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.1.4: + resolution: {integrity: sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + next@15.4.7: resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -9026,6 +9174,10 @@ packages: resolution: {integrity: sha512-sgai5gahy/TiyTiqJEwIFpAuPhmkpt7sGVdRfcmNH53Yc3yI57+zFVmIaqbTST0jP/7tSqZuI0aSllXL2HIw5w==} hasBin: true + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.3: resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -10405,16 +10557,6 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.40(react@19.1.0)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 3.0.8(zod@3.25.76) - ai: 5.0.40(zod@3.25.76) - react: 19.1.0 - swr: 2.3.6(react@19.1.0) - throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.76 - '@ai-sdk/react@2.0.40(react@19.1.0)(zod@4.1.7)': dependencies: '@ai-sdk/provider-utils': 3.0.8(zod@4.1.7) @@ -11176,48 +11318,92 @@ snapshots: dependencies: react: 19.1.1 + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.0 optional: true + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + '@img/sharp-darwin-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.0 optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.0': optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.0': optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.0': optional: true + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + '@img/sharp-libvips-linux-arm@1.2.0': optional: true '@img/sharp-libvips-linux-ppc64@1.2.0': optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + '@img/sharp-libvips-linux-s390x@1.2.0': optional: true + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + '@img/sharp-libvips-linux-x64@1.2.0': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.0': optional: true + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + '@img/sharp-linux-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.0 optional: true + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + '@img/sharp-linux-arm@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.0 @@ -11228,26 +11414,51 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.0 optional: true + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + '@img/sharp-linux-s390x@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.0 optional: true + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + '@img/sharp-linux-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.0 optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 optional: true + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.0 optional: true + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.5 + optional: true + '@img/sharp-wasm32@0.34.3': dependencies: '@emnapi/runtime': 1.4.5 @@ -11256,9 +11467,15 @@ snapshots: '@img/sharp-win32-arm64@0.34.3': optional: true + '@img/sharp-win32-ia32@0.33.5': + optional: true + '@img/sharp-win32-ia32@0.34.3': optional: true + '@img/sharp-win32-x64@0.33.5': + optional: true + '@img/sharp-win32-x64@0.34.3': optional: true @@ -11430,36 +11647,6 @@ snapshots: '@types/react': 19.1.10 react: 19.1.1 - '@merit-systems/echo-next-sdk@0.0.20(next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.76)': - dependencies: - '@merit-systems/echo-react-sdk': 1.0.29(openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.76) - '@merit-systems/echo-typescript-sdk': 1.0.14(zod@3.25.76) - ai: 5.0.40(zod@3.25.76) - jwt-decode: 4.0.0 - next: 15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - swr: 2.3.6(react@19.1.0) - transitivePeerDependencies: - - openai - - zod - - '@merit-systems/echo-react-sdk@1.0.29(openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.76)': - dependencies: - '@ai-sdk/react': 2.0.40(react@19.1.0)(zod@3.25.76) - '@merit-systems/echo-typescript-sdk': 1.0.14(zod@3.25.76) - ai: 5.0.40(zod@3.25.76) - dompurify: 3.2.6 - oidc-client-ts: 3.3.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-oidc-context: 3.3.0(oidc-client-ts@3.3.0)(react@19.1.0) - swr: 2.3.6(react@19.1.0) - optionalDependencies: - openai: 5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) - transitivePeerDependencies: - - zod - '@merit-systems/echo-typescript-sdk@1.0.14(zod@3.25.76)': dependencies: '@ai-sdk/anthropic': 2.0.6(zod@3.25.76) @@ -11470,16 +11657,6 @@ snapshots: transitivePeerDependencies: - zod - '@merit-systems/echo-typescript-sdk@1.0.14(zod@4.1.7)': - dependencies: - '@ai-sdk/anthropic': 2.0.6(zod@4.1.7) - '@ai-sdk/google': 2.0.8(zod@4.1.7) - '@ai-sdk/openai': 2.0.28(zod@4.1.7) - '@openrouter/ai-sdk-provider': 1.2.0(ai@5.0.40(zod@4.1.7))(zod@4.1.7) - ai: 5.0.40(zod@4.1.7) - transitivePeerDependencies: - - zod - '@merit-systems/sdk@0.0.8': dependencies: uuid: 10.0.0 @@ -11736,14 +11913,12 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true + '@next/env@15.1.4': {} + '@next/env@15.4.7': {} '@next/env@15.5.2': {} - '@next/eslint-plugin-next@15.3.1': - dependencies: - fast-glob: 3.3.1 - '@next/eslint-plugin-next@15.4.7': dependencies: fast-glob: 3.3.1 @@ -11752,48 +11927,72 @@ snapshots: dependencies: fast-glob: 3.3.1 + '@next/swc-darwin-arm64@15.1.4': + optional: true + '@next/swc-darwin-arm64@15.4.7': optional: true '@next/swc-darwin-arm64@15.5.2': optional: true + '@next/swc-darwin-x64@15.1.4': + optional: true + '@next/swc-darwin-x64@15.4.7': optional: true '@next/swc-darwin-x64@15.5.2': optional: true + '@next/swc-linux-arm64-gnu@15.1.4': + optional: true + '@next/swc-linux-arm64-gnu@15.4.7': optional: true '@next/swc-linux-arm64-gnu@15.5.2': optional: true + '@next/swc-linux-arm64-musl@15.1.4': + optional: true + '@next/swc-linux-arm64-musl@15.4.7': optional: true '@next/swc-linux-arm64-musl@15.5.2': optional: true + '@next/swc-linux-x64-gnu@15.1.4': + optional: true + '@next/swc-linux-x64-gnu@15.4.7': optional: true '@next/swc-linux-x64-gnu@15.5.2': optional: true + '@next/swc-linux-x64-musl@15.1.4': + optional: true + '@next/swc-linux-x64-musl@15.4.7': optional: true '@next/swc-linux-x64-musl@15.5.2': optional: true + '@next/swc-win32-arm64-msvc@15.1.4': + optional: true + '@next/swc-win32-arm64-msvc@15.4.7': optional: true '@next/swc-win32-arm64-msvc@15.5.2': optional: true + '@next/swc-win32-x64-msvc@15.1.4': + optional: true + '@next/swc-win32-x64-msvc@15.4.7': optional: true @@ -14419,6 +14618,8 @@ snapshots: optionalDependencies: typescript: 5.9.2 + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -17087,26 +17288,6 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.3.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): - dependencies: - '@next/eslint-plugin-next': 15.3.1 - '@rushstack/eslint-patch': 1.11.0 - '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.35.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.35.0(jiti@2.5.1)) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@15.4.7(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2): dependencies: '@next/eslint-plugin-next': 15.4.7 @@ -19476,34 +19657,36 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - next@15.4.7(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.1.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.4.7 + '@next/env': 15.1.4 + '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001721 + busboy: 1.6.0 + caniuse-lite: 1.0.30001743 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.7 - '@next/swc-darwin-x64': 15.4.7 - '@next/swc-linux-arm64-gnu': 15.4.7 - '@next/swc-linux-arm64-musl': 15.4.7 - '@next/swc-linux-x64-gnu': 15.4.7 - '@next/swc-linux-x64-musl': 15.4.7 - '@next/swc-win32-arm64-msvc': 15.4.7 - '@next/swc-win32-x64-msvc': 15.4.7 + '@next/swc-darwin-arm64': 15.1.4 + '@next/swc-darwin-x64': 15.1.4 + '@next/swc-linux-arm64-gnu': 15.1.4 + '@next/swc-linux-arm64-musl': 15.1.4 + '@next/swc-linux-x64-gnu': 15.1.4 + '@next/swc-linux-x64-musl': 15.1.4 + '@next/swc-win32-arm64-msvc': 15.1.4 + '@next/swc-win32-x64-msvc': 15.1.4 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.53.0 - sharp: 0.34.3 + sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@15.5.2(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.4.7(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.5.2 + '@next/env': 15.4.7 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001721 postcss: 8.4.31 @@ -19511,14 +19694,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.2 - '@next/swc-darwin-x64': 15.5.2 - '@next/swc-linux-arm64-gnu': 15.5.2 - '@next/swc-linux-arm64-musl': 15.5.2 - '@next/swc-linux-x64-gnu': 15.5.2 - '@next/swc-linux-x64-musl': 15.5.2 - '@next/swc-win32-arm64-msvc': 15.5.2 - '@next/swc-win32-x64-msvc': 15.5.2 + '@next/swc-darwin-arm64': 15.4.7 + '@next/swc-darwin-x64': 15.4.7 + '@next/swc-linux-arm64-gnu': 15.4.7 + '@next/swc-linux-arm64-musl': 15.4.7 + '@next/swc-linux-x64-gnu': 15.4.7 + '@next/swc-linux-x64-musl': 15.4.7 + '@next/swc-win32-arm64-msvc': 15.4.7 + '@next/swc-win32-x64-msvc': 15.4.7 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.53.0 sharp: 0.34.3 @@ -19779,12 +19962,6 @@ snapshots: transitivePeerDependencies: - encoding - openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): - optionalDependencies: - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) - zod: 3.25.76 - optional: true - openai@5.20.3(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.7): optionalDependencies: ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -20359,11 +20536,6 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - react-oidc-context@3.3.0(oidc-client-ts@3.3.0)(react@19.1.0): - dependencies: - oidc-client-ts: 3.3.0 - react: 19.1.0 - react-oidc-context@3.3.0(oidc-client-ts@3.3.0)(react@19.1.1): dependencies: oidc-client-ts: 3.3.0 @@ -20914,6 +21086,33 @@ snapshots: - supports-color - typescript + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + sharp@0.34.3: dependencies: color: 4.2.3