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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 174 additions & 43 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,180 @@
# OpenClaws.me - Architecture
# OpenClaws — Current Architecture

## Overview
Hosted OpenClaw agent platform. Users sign up, get an AI agent on Telegram/Discord/WhatsApp, connect integrations via Composio OAuth.

OpenClaws is a hosted personal-agent platform. A user signs in on `openclaws.biz`, starts a dedicated OpenClaw gateway, manages provider keys and channels from the dashboard, and reaches that agent through the hosted web app plus paired messaging surfaces.

This document describes the **current implementation**, not the original Railway-era plan.

## Stack
- **Framework:** Next.js 15 (App Router)
- **Hosting:** Vercel

- **Framework:** Next.js 16 (App Router)
- **Hosting:** Vercel for the web app
- **Auth:** NextAuth.js (Google OAuth)
- **Database:** Supabase (PostgreSQL)
- **Billing:** Stripe Checkout + Customer Portal
- **Agent Provisioning:** Railway API (Docker containers)
- **Integrations:** Composio SDK
- **Styling:** Tailwind CSS

## Pages
- `/` - Landing page (already built, copy from openclaws-landing/)
- `/login` - Google OAuth sign-in
- `/dashboard` - Main chat interface (like TrustClaw)
- `/dashboard/toolkits` - Composio integrations (like TrustClaw toolkits)
- `/dashboard/settings` - User settings, billing, model selection
- `/api/auth/[...nextauth]` - NextAuth routes
- `/api/stripe/webhook` - Stripe webhook handler
- `/api/stripe/checkout` - Create checkout session
- `/api/agent/provision` - Provision OpenClaw instance on Railway

## User Flow
1. Land on openclaws.me
2. Click "Get Started" -> Google OAuth
3. Free tier: basic chat, limited messages
4. Click "Upgrade" -> Stripe $29/mo
5. Premium: better models, unlimited, toolkits access
6. Toolkits: connect Gmail, GitHub, Slack etc. via Composio OAuth

## Agent Provisioning
Each user gets a dedicated OpenClaw Docker container on Railway:
- Docker image: ghcr.io/openclaw/openclaw:latest
- Configured with user's chosen model + channel
- Composio integrations passed as env vars
- Health monitoring via Railway API

## Free vs Premium
| Feature | Free | Premium ($29/mo) |
|---------|------|-------------------|
| Messages | 50/day | Unlimited |
| Model | Sonnet 4.5 | Opus 4.5 + GPT-4o + Gemini |
| Integrations | 3 | Unlimited |
| Channels | 1 | All (Telegram, Discord, WhatsApp) |
| Mission Control | Basic | Full dashboard |
- **Billing:** Stripe
- **Gateway provisioning:** Fly Machines
- **Observability:** Sentry web + server SDKs, Sentry webhook -> Linear + Paperclip issue creation
- **Agent runtime:** Dedicated OpenClaw gateway per customer

## Main Product Surfaces

- `/` - marketing / landing page
- `/login` - Google sign-in
- `/dashboard` - primary app shell
- `/dashboard/settings` - model selection, provider keys, billing, personalization
- `/dashboard/toolkits` - Composio-connected integrations
- `/api/instance` - create, fetch, and destroy a customer gateway instance
- `/api/gateway/*` - gateway control-plane actions used by the dashboard
- `/api/webhooks/sentry` - receives Sentry issue webhooks and opens bugs in Linear and Paperclip

## Provisioning Flow

Gateway provisioning is handled in `src/lib/fly.ts` and invoked from `src/app/api/instance/route.ts`.

### What gets created

For each provisioned customer:

1. A Fly app named from `FLY_APP_PREFIX` + a unique slug
2. A persistent Fly volume mounted at `/data`
3. A dedicated Fly Machine running the OpenClaw gateway image
4. A custom subdomain at `https://{slug}.openclaws.biz`
5. A generated gateway token stored in the OpenClaws database

### Runtime image and startup behavior

The machine currently runs:

- Image: `ghcr.io/openclaw/openclaw:main`
- Command path: `node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan`

Before boot, provisioning seeds `/data/openclaw.json` and writes an `IDENTITY.md` file so the hosted agent introduces itself as an OpenClaws agent rather than a raw upstream model persona.

### Gateway config seeded at provision time

Provisioning currently sets:

- `gateway.controlUi.allowedOrigins` for the hosted domains
- `gateway.controlUi.allowInsecureAuth = false`
- `skills.entries.composio.enabled = true`
- a minimal default `agents.list` entry for the customer agent

### Provider-key behavior

OpenClaws passes through user-supplied model keys when available:

- `ANTHROPIC_API_KEY`
- `ANTHROPIC_OAUTH_TOKEN`
- `OPENAI_API_KEY`

If the user has not supplied Anthropic credentials and the platform has one configured, OpenClaws mints a short-lived proxy token and points the gateway at the hosted Anthropic proxy.

### Composio behavior

If Composio is configured, provisioning also:

- passes `COMPOSIO_API_KEY`
- passes a per-user `COMPOSIO_ENTITY_ID`
- installs a lightweight `composio-exec` bridge into the customer gateway
- installs a small Composio skill into persistent storage

## Security / Network Posture

### Current behavior

The provisioned gateway is intentionally exposed through Fly on ports 80/443 and the OpenClaw process binds with `--bind lan` so Fly’s proxy can reach it.

Provisioning also starts a background loop that repeatedly runs:

`openclaw devices approve --latest`

This means device pairing is currently optimized for low-friction hosted onboarding rather than strict manual approval.

### Why this matters

This is a deliberate **managed-hosting exception** to the stricter internal OpenClaw posture of loopback-only listeners plus explicit device approval.

Operational upside:

- much simpler end-user onboarding
- fewer support tickets for first-time pairing
- works with Fly’s public routing model

Tradeoff / risk:

- weaker pairing friction than a manually approved device flow
- background approval can hide pairing-state problems instead of surfacing them clearly
- security expectations must be documented accurately so support and engineering do not assume loopback-only behavior

If OpenClaws later tightens posture, the likely path is:

- remove auto-approval loop
- keep hosted ingress but require explicit pairing approval
- make sleeping / waking / approval state visible in the dashboard

## Pairing Approval Path

The dashboard can call `POST /api/gateway/approve-pairing`.

Current behavior:

- looks up the customer’s Fly app / machine / gateway token
- verifies the Fly machine is actually running
- executes `openclaw devices approve --latest --json` inside the machine
- returns a specific response when there is no pending request
- returns a specific response when the machine is stopped / sleeping instead of a generic failure

### Operational caveat

This route still depends on Fly machine exec, so it is inherently more fragile than an in-process control-plane API. If Fly exec is degraded or the machine is asleep, approval can fail even though the underlying gateway config is fine.

## Observability and Support

### Sentry

OpenClaws initializes Sentry in both client and server contexts:

- `sentry.client.config.ts`
- `sentry.server.config.ts`

Current safeguards include:

- disabling default PII sending
- masking client-side inputs in session replay
- stripping provider API headers from server breadcrumbs
- scrubbing common API key patterns from client error messages

### Sentry -> Linear + Paperclip

`/api/webhooks/sentry` verifies the Sentry HMAC signature and creates bug tickets in:

- **Linear** for product/project tracking
- **Paperclip** for agent assignment / triage

Routing is based on the Sentry project slug. For `openclaws`, bugs are routed into the OpenClaws Linear and Paperclip projects and assigned to the OpenClaws lead agent when configured.

### Current support gap

OpenClaws is good at turning thrown errors into tickets, but thinner on customer-visible diagnostics for provisioning and pairing. Support still relies heavily on:

- Fly machine state
- Fly exec health
- Sentry traces/errors
- DB instance rows

The dashboard does not yet expose a rich, first-party support view for why onboarding or pairing failed.

## Known Gaps / Follow-Up Work

1. **Pairing approval should avoid shelling through Fly exec when possible.**
A first-class control endpoint on the gateway would be more reliable than exec-ing CLI commands into the machine.

2. **Provisioning posture needs an explicit product decision.**
The current hosted setup favors onboarding convenience over strict manual device authorization. That should remain documented unless/until product changes it.

3. **Support visibility is still thin.**
The dashboard should eventually expose machine state, last provisioning error, and pairing status more directly.

4. **Keep this file honest.**
If provisioning or network posture changes, update this document in the same PR.
35 changes: 30 additions & 5 deletions src/app/api/composio/toolkits/connect/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';
import * as Sentry from '@sentry/nextjs';
import { ComposioError } from 'composio-core';
import { authOptions } from '@/lib/auth';
import { rateLimit, rateLimitResponse } from '@/lib/rate-limit';
import { getComposioClient, getComposioEntityId, isComposioConfigured } from '@/lib/composio';
Expand All @@ -18,6 +19,19 @@ function buildRedirectUri() {
return `${base.replace(/\/+$/, '')}/dashboard/toolkits`;
}

function getComposioErrorStatus(error: unknown) {
if (!(error instanceof ComposioError)) return null;

const statusCode = (error.metadata as { statusCode?: unknown } | undefined)?.statusCode;
return typeof statusCode === 'number' ? statusCode : null;
}

function getComposioErrorMessage(error: unknown) {
if (!(error instanceof ComposioError)) return null;

return error.message?.trim() || error.description?.trim() || null;
}

export async function POST(request: Request) {
const session = await getServerSession(authOptions);

Expand All @@ -44,12 +58,15 @@ export async function POST(request: Request) {
const entityId = getComposioEntityId(session);
const entity = getComposioClient().getEntity(entityId);
const redirectUri = buildRedirectUri();
const connectionParams = redirectUri
? {
appName,
redirectUri,
config: { redirectUrl: redirectUri },
}
: { appName };

const requestResult = await entity.initiateConnection({
appName,
redirectUri,
config: { redirectUrl: redirectUri },
});
const requestResult = await entity.initiateConnection(connectionParams);

return NextResponse.json({
appName,
Expand All @@ -58,6 +75,14 @@ export async function POST(request: Request) {
redirectUrl: requestResult.redirectUrl,
});
} catch (error) {
const statusCode = getComposioErrorStatus(error);
if (statusCode && statusCode >= 400 && statusCode < 500) {
return NextResponse.json(
{ error: getComposioErrorMessage(error) ?? 'Unable to start that Composio connection.' },
{ status: statusCode },
);
}
Comment on lines 77 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

composio-core npm package ComposioError class documentation

💡 Result:

The composio-core npm package refers to @composio/core (latest v0.6.6 as of March 2026), the TypeScript SDK for Composio platform. It includes a ComposioError class for handling SDK and API errors. ComposioError is the base exception class thrown by the SDK for issues like API failures, invalid requests, server errors, authentication problems, and more. It is used in error handling for backend API responses, custom errors via CEG (Composio Error Generator), and specific cases like SERVER_UNREACHABLE. Key details from sources: - Thrown in catch blocks, e.g., console.error('❌ Error:', error) where error is ComposioError instance. - Handles HTTP status codes (400 Bad Request, 429 Rate Limit, 5xx Server Errors) with structured responses including message, status, code, request_id. - Maps backend errors to SDK error codes (e.g., API_KEY_INVALID, SERVER_UNREACHABLE). - Used in functions like getCustomError, handleAllError, throwAPIError. - Example crash report: "ComposioError: undefined is not an object (evaluating 'predefinedError.message')" in v0.5.39, fixed in later versions. No public TypeDoc or detailed API docs for ComposioError found in official docs (https://docs.composio.dev/errors/error-handling focuses on API errors). Source code in GitHub repo ComposioHQ/composio (js/src/sdk/utils/error.ts or similar), introduced/enhanced in PRs like #812, #928. For usage: try { // SDK operations } catch (error) { if (error instanceof ComposioError) { console.error(error.message); // Human-readable error details } } Primary package is @composio/core (npm install @composio/core). Older/legacy composio-core exists but is deprecated.

Citations:


🏁 Script executed:

# Search for ComposioError class definition and related error handling
rg "class ComposioError|interface ComposioError" --type ts --type js

Repository: Alex-Alaniz/openclaws

Length of output: 47


🌐 Web query:

ComposioError class structure statusCode property @composio/core GitHub source

💡 Result:

The ComposioError class exists in both Python and TypeScript SDKs of @composio/core (ComposioHQ/composio GitHub repo). It is used to wrap API errors with structured information including HTTP status codes. In Python (composio package): from composio.exceptions import ComposioError. Used in error handling examples, e.g., except ComposioError as e. No explicit class definition found in searched files, but imported from composio.exceptions module. Likely defined there with properties for error details. In TypeScript (@composio/core): ComposioError class introduced in PR #812 (js/src/sdk/utils/error.ts), with CEG (Composio Error Generator) for standardized handling. Throws ComposioError instances via CEG.throwCustomError, handleAllError, etc. Maps backend HTTP status codes (400, 401, 500, etc.) to SDK error codes via tables like API_TO_SDK_ERROR_CODE or BE_STATUS_CODE_TO_SDK_ERROR_CODES. The statusCode property: Official docs show API error responses have "status": HTTP status code (e.g., 400). SDK ComposioError extracts/embeds this from axiosError.response.status during error formatting (getAPIErrorDetails in formatter.ts). Confirmed in error handling PRs (#928, #1089, #1347, #838) and bug #1716 referencing ComposioError construction from HTTP errors. No direct "statusCode" property shown in snippets, but effectively accessible via error.status or similar from parsed response. Repo structure: Python in /python/composio/, TS in /js/src/sdk/utils/errors/. Default branch: next. Latest activity 2026-03-25.

Citations:


🏁 Script executed:

rg "getComposioErrorStatus" --type ts --type js -A 10

Repository: Alex-Alaniz/openclaws

Length of output: 1957


🏁 Script executed:

# Also search for imports in the file we're reviewing
rg "getComposioErrorStatus|getComposioErrorMessage" -B 5 -A 3

Repository: Alex-Alaniz/openclaws

Length of output: 2444


🌐 Web query:

ComposioError metadata property structure @composio/core GitHub source code

💡 Result:

The ComposioError in @composio/core is a custom TypeScript error class introduced for standardized error handling in the Composio JavaScript/TypeScript SDK. It is defined in the SDK source code at js/src/sdk/utils/error.ts (or similar path based on repository structure). Key details from GitHub source code analysis (ComposioHQ/composio repository): - ComposioError provides helpful properties including: - name: Error type name (e.g., 'ComposioToolNotFoundError') - message: Human-readable error message - code: Error code from predefined constants (e.g., API_KEY_INVALID, SERVER_UNAVAILABLE) - It works alongside CEG (Composio Error Generator) class, which handles error creation, mapping backend HTTP status codes to SDK error codes, and throwing custom errors via methods like CEG.throwCustomError and CEG.handleError. - Backend API errors are structured as: { "error": { "message": string, "status": number (HTTP status), "request_id": string, "suggested_fix"?: string } } - No explicit 'metadata' property is directly mentioned in ComposioError itself in available snippets. However, error handling enhancements include detailed context like sessionId in SDK context, API request details, and backend response parsing (type, name, message from formatter.ts). Metadata-like details may be captured in error payloads for telemetry (exceptionHandler.ts) or backend responses. The structure emphasizes descriptive messages, type-safe errors, and integration with backend responses for better debugging. For exact current code, view js/src/sdk/utils/error.ts in the ComposioHQ/composio GitHub repo (latest at @composio/core@0.3.3+). Python SDK has analogous ComposioError with similar properties.

Citations:


🏁 Script executed:

# Search for any documentation or types of ComposioError in the repo
rg "interface ComposioError|type ComposioError" --type ts --type js

Repository: Alex-Alaniz/openclaws

Length of output: 47


🌐 Web query:

site:github.com/ComposioHQ/composio "error.ts" ComposioError class definition

💡 Result:

The ComposioError class is defined in the Composio JavaScript/TypeScript SDK, specifically in the file js/src/sdk/error.ts (or a similar path like js/src/sdk/utils/error.ts based on references). It was introduced as part of enhanced error handling in PR #812 (merged Nov 2024), alongside the CEG (Composio Error Generator) class for standardized error management. The class is used across SDK modules like apps.ts, integrations.ts, etc., via CEG.handleError. Subsequent PRs (e.g., #838, #928, #1089) updated error codes, messages, mappings (e.g., API_TO_SDK_ERROR_CODE), and handling functions like handleAllError and throwAPIError in error.ts. No full source code or exact class definition (properties, constructor, methods) is available in public search results or PR descriptions; only references confirm its existence and purpose in the Composio SDK for throwing structured errors with details like type, name, message, description, and possible fixes.

Citations:


🏁 Script executed:

# Check if there are any type definitions imported from composio in the file
rg "import.*ComposioError|from.*composio" src/app/api/composio/toolkits/connect/route.ts -A 2

Repository: Alex-Alaniz/openclaws

Length of output: 347


🌐 Web query:

composio-core ComposioError properties structure statusCode status code npm types

💡 Result:

The ComposioError class in the composio-core (or @composio/core) TypeScript SDK is a custom error class used for standardized error handling. It is introduced and used in the JS/TS SDK source code located at paths like js/src/sdk/utils/errors/src/error.ts or similar, as referenced in multiple GitHub PRs. Key properties and structure, synthesized from error examples, GitHub issues, PRs, and docs: - errorCode or code: String identifier like "BACKEND::SERVER_ERROR" or "SERVER_UNREACHABLE" (used for programmatic handling). - message: Human-readable error description, e.g., "💥 Oops! Something went wrong on our end." or "Error Code: BACKEND::SERVER_ERROR". - status or statusCode: HTTP status code from backend response, e.g., 400, 500 (camelCase likely in TS types, matching API response "status": 400). - request_id: Unique request identifier for support. - suggested_fix: Optional guidance to resolve the error. - description: Additional details, sometimes from backend. - Stack Trace: Standard Error stack. Example from real usage (GitHub issue): ComposioError: Error Code: BACKEND::SERVER_ERROR Message: 💥 Oops! Something went wrong on our end. Description: "Cannot read properties of null (reading 'join')" in (URL: /api/v1/connectedAccounts, Status: 500) Suggested Fix: Please try again later. If the issue persists, contact support. Stack Trace: Error at new ComposioError The backend API returns errors as { "error": { "message": string, "status": number, "request_id": string, "suggested_fix"?: string } }, and the SDK wraps this into ComposioError instances using classes like CEG (Composio Error Generator) via methods like CEG.throwCustomError, handleError, throwAPIError. NPM package: composio-core (legacy, last ~1yr ago) or current @composio/core. No public types.d.ts snippet found in searches, but confirmed via GitHub source/PRs and error logs. Mintlify docs mention: name (error type like 'ComposioToolNotFoundError'), message. For full TypeScript interface, check the repo source: https://github.com/ComposioHQ/composio/tree/master/js/src/sdk/utils/errors (inferred path).

Citations:


🏁 Script executed:

# Check if there are other usages of ComposioError in the codebase to see common patterns
rg "ComposioError" --type ts --type js -B 2 -A 2 | head -50

Repository: Alex-Alaniz/openclaws

Length of output: 2181


🌐 Web query:

"composio-core" OR "@composio/core" npm "statusCode" OR "status" error property direct

💡 Result:

In the @composio/core npm package (the current core Composio SDK for TypeScript/Node.js), API errors from the Composio backend are returned as structured error objects that include a "status" property containing the HTTP status code (e.g., 400). These errors are typically thrown when making API calls like tools.get or tools.execute and caught in try-catch blocks, as shown in the official npm README examples: console.error('❌ Error:', error); The backend error format is: { "error": { "message": "description", "status": 400, "request_id": "...", "suggested_fix": "..." } } The SDK processes these into custom error instances with properties like message, name, type, and status accessible directly on the error object (error.status). This is confirmed by official docs on error handling and multiple GitHub PRs improving SDK error parsing for backend responses, including handling status codes like 400/404 in files like error.ts, formatter.ts, and constants.ts. Legacy package "composio-core" (older versions) exists but is not current; use @composio/core (v0.6+ as of 2026).

Citations:


🏁 Script executed:

# Search for any test files or examples in the repo that show error property access
rg "error\.status|error\.statusCode|error\.metadata" --type ts --type js -B 2 -A 2

Repository: Alex-Alaniz/openclaws

Length of output: 508


The property path error.metadata.statusCode is not documented and likely incorrect.

ComposioError is the correct class name. However, based on the Composio SDK documentation and error structure, the status code should be accessed directly as error.status or error.statusCode, not through a metadata object. The backend error format shows status as a top-level property, and all documentation indicates direct property access.

The inconsistency is evident in the same file: getComposioErrorMessage correctly accesses error.message and error.description directly, while getComposioErrorStatus uses the undocumented error.metadata.statusCode path with defensive type casting.

Update getComposioErrorStatus to:

function getComposioErrorStatus(error: unknown) {
  if (!(error instanceof ComposioError)) return null;
  
  const statusCode = (error as { status?: unknown }).status;
  return typeof statusCode === 'number' ? statusCode : null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/composio/toolkits/connect/route.ts` around lines 77 - 84, Update
getComposioErrorStatus to stop reading the undocumented
error.metadata.statusCode path and instead check the thrown error is a
ComposioError and read the top-level status (or statusCode) field directly; in
getComposioErrorStatus, replace the metadata access with a guarded read like
(error as { status?: unknown }).status and return it only if it's a number,
otherwise return null (this keeps behavior consistent with
getComposioErrorMessage which reads error.message/description).


Sentry.captureException(error);
return NextResponse.json(
{ error: 'Failed to initiate Composio connection.' },
Expand Down
61 changes: 22 additions & 39 deletions src/app/api/gateway/approve-pairing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';
import * as Sentry from '@sentry/nextjs';
import { authOptions, getUserEmail } from '@/lib/auth';
import { approveLatestPairingRequest } from '@/lib/fly';
import { getInstanceByUserId } from '@/lib/supabase';
import { rateLimit, rateLimitResponse } from '@/lib/rate-limit';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

const FLY_API_BASE = 'https://api.machines.dev/v1';

export async function POST() {
const session = await getServerSession(authOptions);
const email = getUserEmail(session ?? {});
Expand All @@ -32,51 +31,35 @@ export async function POST() {
}

try {
const authHeader = flyToken.startsWith('FlyV1 ') ? flyToken : `Bearer ${flyToken}`;

// Exec openclaw devices approve --latest on the user's Fly machine
const execRes = await fetch(
`${FLY_API_BASE}/apps/${instance.fly_app_name}/machines/${instance.fly_machine_id}/exec`,
{
method: 'POST',
headers: {
Authorization: authHeader,
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: [
'sh', '-c',
`OPENCLAW_GATEWAY_PORT=3000 OPENCLAW_GATEWAY_TOKEN=${instance.gateway_token} openclaw devices approve --latest --json`,
],
timeout: 10,
}),
signal: AbortSignal.timeout(15000),
},
const result = await approveLatestPairingRequest(
instance.fly_app_name,
instance.fly_machine_id,
instance.gateway_token,
);

if (!execRes.ok) {
const errText = await execRes.text().catch(() => '');
Sentry.captureMessage('Fly exec failed for approve-pairing', {
level: 'warning',
extra: { status: execRes.status, body: errText, app: instance.fly_app_name },
});
return NextResponse.json({ error: 'Failed to reach gateway' }, { status: 502 });
if (result.reason === 'machine_not_running') {
return NextResponse.json(
{
approved: false,
error: 'Gateway is not currently running. Open your agent first, then try pairing again.',
machineState: result.machineState,
},
{ status: 409 },
);
}

const result = await execRes.json().catch(() => ({})) as { stdout?: string; stderr?: string; exit_code?: number };

if (result.exit_code !== 0) {
// No pending requests is not an error from the user's perspective
const stderr = result.stderr ?? '';
if (stderr.includes('no pending') || stderr.includes('No pending')) {
return NextResponse.json({ approved: false, message: 'No pending pairing requests' });
}
return NextResponse.json({ error: 'No pending pairing requests to approve' }, { status: 404 });
if (result.reason === 'no_pending') {
return NextResponse.json({ approved: false, message: 'No pending pairing requests' });
}

return NextResponse.json({ approved: true });
} catch (error) {
Sentry.captureException(error);
Sentry.captureException(error, {
extra: {
app: instance.fly_app_name,
machineId: instance.fly_machine_id,
},
});
return NextResponse.json({ error: 'Failed to approve pairing' }, { status: 500 });
}
}
Loading