Enable Stripe checkout and portal flows in the billing UI, and harden OpenAPI type extraction plus cache invalidation so tool hints remain accurate on real-world specs.
Unified · Split
| @@ -13,6 +13,14 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 12 unmodified lines | |||
| 13 | # WORKOS_COOKIE_PASSWORD=<32+ char random string, e.g. openssl rand -base64 32> | 13 | # WORKOS_COOKIE_PASSWORD=<32+ char random string, e.g. openssl rand -base64 32> |
| 14 | # WORKOS_WEBHOOK_SECRET=... | 14 | # WORKOS_WEBHOOK_SECRET=... |
| 15 | 15 | ||
| 16 | # ── Stripe Billing (optional — required for checkout/portal) ── | ||
| 17 | # STRIPE_SECRET_KEY=sk_test_... | ||
| 18 | # STRIPE_WEBHOOK_SECRET=whsec_... | ||
| 19 | # STRIPE_PRICE_ID=price_... | ||
| 20 | # BILLING_SUCCESS_URL=http://localhost:3000/organization?tab=billing&success=true | ||
| 21 | # BILLING_CANCEL_URL=http://localhost:3000/organization?tab=billing&canceled=true | ||
| 22 | # BILLING_RETURN_URL=http://localhost:3000/organization?tab=billing | ||
| 23 | |||
| 16 | # ── Discord bot (optional — omit to skip bot startup) ── | 24 | # ── Discord bot (optional — omit to skip bot startup) ── |
| 17 | # DISCORD_BOT_TOKEN=... | 25 | # DISCORD_BOT_TOKEN=... |
| 18 | 26 | ||
| @@ -106,6 +106,7 @@ · 105 unmodified lines | |||
| 106 | "radix-ui": "^1.4.3", | 106 | "radix-ui": "^1.4.3", |
| 107 | "react": "19.2.3", | 107 | "react": "19.2.3", |
| 108 | "react-dom": "19.2.3", | 108 | "react-dom": "19.2.3", |
| 109 | "shiki": "^3.22.0", | ||
| 109 | "sonner": "^2.0.7", | 110 | "sonner": "^2.0.7", |
| 110 | "streamdown": "^2.1.0", | 111 | "streamdown": "^2.1.0", |
| 111 | "tailwind-merge": "^3.4.0", | 112 | "tailwind-merge": "^3.4.0", |
| @@ -6,6 +6,7 @@ const nextConfig: NextConfig = { · 5 unmodified lines | |||
| 6 | // This lets us keep a single root .env without NEXT_PUBLIC_ prefixes. | 6 | // This lets us keep a single root .env without NEXT_PUBLIC_ prefixes. |
| 7 | NEXT_PUBLIC_CONVEX_URL: process.env.CONVEX_URL, | 7 | NEXT_PUBLIC_CONVEX_URL: process.env.CONVEX_URL, |
| 8 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, | 8 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, |
| 9 | NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID, | ||
| 9 | }, | 10 | }, |
| 10 | }; | 11 | }; |
| 11 | 12 | ||
| @@ -28,6 +28,7 @@ · 27 unmodified lines | |||
| 28 | "radix-ui": "^1.4.3", | 28 | "radix-ui": "^1.4.3", |
| 29 | "react": "19.2.3", | 29 | "react": "19.2.3", |
| 30 | "react-dom": "19.2.3", | 30 | "react-dom": "19.2.3", |
| 31 | "shiki": "^3.22.0", | ||
| 31 | "sonner": "^2.0.7", | 32 | "sonner": "^2.0.7", |
| 32 | "streamdown": "^2.1.0", | 33 | "streamdown": "^2.1.0", |
| 33 | "tailwind-merge": "^3.4.0", | 34 | "tailwind-merge": "^3.4.0", |
| @@ -1,20 +1,59 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useMemo, useState } from "react"; | 3 | import { useMemo, useState } from "react"; |
| 4 | import { useSearchParams } from "next/navigation"; | ||
| 5 | import { useAction, useMutation, useQuery } from "convex/react"; | ||
| 4 | import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react"; | 6 | import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react"; |
| 5 | import { PageHeader } from "@/components/page-header"; | 7 | import { PageHeader } from "@/components/page-header"; |
| 8 | import { Badge } from "@/components/ui/badge"; | ||
| 6 | import { Button } from "@/components/ui/button"; | 9 | import { Button } from "@/components/ui/button"; |
| 7 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | 10 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
| 11 | import { Input } from "@/components/ui/input"; | ||
| 12 | import { convexApi } from "@/lib/convex-api"; | ||
| 8 | import { useSession } from "@/lib/session-context"; | 13 | import { useSession } from "@/lib/session-context"; |
| 9 | 14 | ||
| 10 | interface BillingViewProps { | 15 | interface BillingViewProps { |
| 11 | showHeader?: boolean; | 16 | showHeader?: boolean; |
| 12 | } | 17 | } |
| 13 | 18 | ||
| 19 | type BillingSummary = { | ||
| 20 | customer: { | ||
| 21 | stripeCustomerId: string; | ||
| 22 | } | null; | ||
| 23 | subscription: { | ||
| 24 | stripeSubscriptionId: string; | ||
| 25 | stripePriceId: string; | ||
| 26 | status: string; | ||
| 27 | currentPeriodEnd: number | null; | ||
| 28 | cancelAtPeriodEnd: boolean; | ||
| 29 | } | null; | ||
| 30 | seats: { | ||
| 31 | billableMembers: number; | ||
| 32 | desiredSeats: number; | ||
| 33 | lastAppliedSeats: number | null; | ||
| 34 | }; | ||
| 35 | sync: { | ||
| 36 | status: "ok" | "error" | "pending"; | ||
| 37 | lastSyncAt: number | null; | ||
| 38 | error: string | null; | ||
| 39 | }; | ||
| 40 | }; | ||
| 41 | |||
| 42 | function formatTimestamp(value: number | null | undefined): string { | ||
| 43 | if (!value || !Number.isFinite(value)) { | ||
| 44 | return "-"; | ||
| 45 | } | ||
| 46 | |||
| 47 | const millis = value > 1_000_000_000_000 ? value : value * 1_000; | ||
| 48 | return new Date(millis).toLocaleString(); | ||
| 49 | } | ||
| 50 | |||
| 14 | export function BillingView({ showHeader = true }: BillingViewProps) { | 51 | export function BillingView({ showHeader = true }: BillingViewProps) { |
| 15 | const { context, organizations, workspaces } = useSession(); | 52 | const { context, organizations, organizationsLoading, workspaces } = useSession(); |
| 16 | const [actionState, setActionState] = useState<"idle" | "running" | "error">("idle"); | 53 | const searchParams = useSearchParams(); |
| 54 | const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle"); | ||
| 17 | const [actionMessage, setActionMessage] = useState<string | null>(null); | 55 | const [actionMessage, setActionMessage] = useState<string | null>(null); |
| 56 | const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? ""); | ||
| 18 | 57 | ||
| 19 | const derivedOrganizationId = context | 58 | const derivedOrganizationId = context |
| 20 | ? workspaces.find((workspace) => workspace.id === context.workspaceId)?.organizationId ?? null | 59 | ? workspaces.find((workspace) => workspace.id === context.workspaceId)?.organizationId ?? null |
| @@ -25,13 +64,98 @@ export function BillingView({ showHeader = true }: BillingViewProps) { · 4 unmodified lines | |||
| 25 | () => organizations.find((organization) => organization.id === effectiveOrganizationId) ?? null, | 64 | () => organizations.find((organization) => organization.id === effectiveOrganizationId) ?? null, |
| 26 | [organizations, effectiveOrganizationId], | 65 | [organizations, effectiveOrganizationId], |
| 27 | ); | 66 | ); |
| 67 | |||
| 28 | const canManageBilling = activeOrganization | 68 | const canManageBilling = activeOrganization |
| 29 | ? ["owner", "billing_admin"].includes(activeOrganization.role) | 69 | ? ["owner", "admin", "billing_admin"].includes(activeOrganization.role) |
| 30 | : false; | 70 | : false; |
| 31 | 71 | ||
| 32 | const notReady = () => { | 72 | const summary = useQuery( |
| 33 | setActionState("error"); | 73 | convexApi.billing.getSummary, |
| 34 | setActionMessage("Billing actions are waiting on backend endpoints."); | 74 | effectiveOrganizationId |
| 75 | ? { | ||
| 76 | organizationId: effectiveOrganizationId, | ||
| 77 | sessionId: context?.sessionId ?? undefined, | ||
| 78 | } | ||
| 79 | : "skip", | ||
| 80 | ) as BillingSummary | undefined; | ||
| 81 | |||
| 82 | const createSubscriptionCheckout = useAction(convexApi.billing.createSubscriptionCheckout); | ||
| 83 | const createCustomerPortal = useAction(convexApi.billing.createCustomerPortal); | ||
| 84 | const retrySeatSync = useMutation(convexApi.billing.retrySeatSync); | ||
| 85 | |||
| 86 | const checkoutSuccess = searchParams.get("success") === "true"; | ||
| 87 | const checkoutCanceled = searchParams.get("canceled") === "true"; | ||
| 88 | |||
| 89 | const handleStartCheckout = async () => { | ||
| 90 | if (!effectiveOrganizationId || !canManageBilling) { | ||
| 91 | return; | ||
| 92 | } | ||
| 93 | |||
| 94 | if (!priceId.trim()) { | ||
| 95 | setActionState("error"); | ||
| 96 | setActionMessage("Set a Stripe price ID before starting checkout."); | ||
| 97 | return; | ||
| 98 | } | ||
| 99 | |||
| 100 | setActionState("running"); | ||
| 101 | setActionMessage(null); | ||
| 102 | try { | ||
| 103 | const session = await createSubscriptionCheckout({ | ||
| 104 | organizationId: effectiveOrganizationId, | ||
| 105 | priceId: priceId.trim(), | ||
| 106 | sessionId: context?.sessionId ?? undefined, | ||
| 107 | }); | ||
| 108 | |||
| 109 | if (session.url) { | ||
| 110 | window.location.assign(session.url); | ||
| 111 | return; | ||
| 112 | } | ||
| 113 | |||
| 114 | setActionState("success"); | ||
| 115 | setActionMessage("Checkout session created."); | ||
| 116 | } catch (error) { | ||
| 117 | setActionState("error"); | ||
| 118 | setActionMessage(error instanceof Error ? error.message : "Failed to start checkout"); | ||
| 119 | } | ||
| 120 | }; | ||
| 121 | |||
| 122 | const handleOpenPortal = async () => { | ||
| 123 | if (!effectiveOrganizationId || !canManageBilling) { | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | |||
| 127 | setActionState("running"); | ||
| 128 | setActionMessage(null); | ||
| 129 | try { | ||
| 130 | const result = await createCustomerPortal({ | ||
| 131 | organizationId: effectiveOrganizationId, | ||
| 132 | sessionId: context?.sessionId ?? undefined, | ||
| 133 | }); | ||
| 134 | window.location.assign(result.url); | ||
| 135 | } catch (error) { | ||
| 136 | setActionState("error"); | ||
| 137 | setActionMessage(error instanceof Error ? error.message : "Failed to open billing portal"); | ||
| 138 | } | ||
| 139 | }; | ||
| 140 | |||
| 141 | const handleRetrySync = async () => { | ||
| 142 | if (!effectiveOrganizationId || !canManageBilling) { | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | |||
| 146 | setActionState("running"); | ||
| 147 | setActionMessage(null); | ||
| 148 | try { | ||
| 149 | await retrySeatSync({ | ||
| 150 | organizationId: effectiveOrganizationId, | ||
| 151 | sessionId: context?.sessionId ?? undefined, | ||
| 152 | }); | ||
| 153 | setActionState("success"); | ||
| 154 | setActionMessage("Seat sync queued."); | ||
| 155 | } catch (error) { | ||
| 156 | setActionState("error"); | ||
| 157 | setActionMessage(error instanceof Error ? error.message : "Failed to queue seat sync"); | ||
| 158 | } | ||
| 35 | }; | 159 | }; |
| 36 | 160 | ||
| 37 | if (!effectiveOrganizationId) { | 161 | if (!effectiveOrganizationId) { |
| @@ -51,41 +175,76 @@ export function BillingView({ showHeader = true }: BillingViewProps) { · 13 unmodified lines | |||
| 51 | <div className="space-y-6"> | 175 | <div className="space-y-6"> |
| 52 | {showHeader ? <PageHeader title="Billing" description="Manage subscription, seats, and billing portal access" /> : null} | 176 | {showHeader ? <PageHeader title="Billing" description="Manage subscription, seats, and billing portal access" /> : null} |
| 53 | 177 | ||
| 54 | <Card> | 178 | {checkoutSuccess ? ( |
| 55 | <CardContent className="p-6 text-sm text-muted-foreground"> | 179 | <Card> |
| 56 | Billing backend wiring is still in progress. This screen is ready for `billing.getSummary`, | 180 | <CardContent className="p-4 text-sm text-terminal-green"> |
| 57 | `billing.createSubscriptionCheckout`, `billing.createCustomerPortal`, and `billing.retrySeatSync`. | 181 | Checkout completed. Billing updates may take a few seconds while Stripe webhooks process. |
| 58 | </CardContent> | 182 | </CardContent> |
| 59 | </Card> | 183 | </Card> |
| 184 | ) : null} | ||
| 60 | 185 | ||
| 61 | <Card> | 186 | {checkoutCanceled ? ( |
| 62 | <CardContent className="p-4 text-sm text-muted-foreground flex items-center gap-2"> | 187 | <Card> |
| 63 | <RefreshCcw className="h-4 w-4" /> | 188 | <CardContent className="p-4 text-sm text-muted-foreground"> |
| 64 | Billing sync pending. Numbers may be briefly stale during webhook processing. | 189 | Checkout canceled. You can restart checkout at any time. |
| 65 | </CardContent> | 190 | </CardContent> |
| 66 | </Card> | 191 | </Card> |
| 192 | ) : null} | ||
| 67 | 193 | ||
| 68 | <Card> | 194 | <Card> |
| 69 | <CardContent className="p-4 text-sm text-destructive flex items-center justify-between gap-3"> | 195 | <CardContent className="p-4 text-sm text-muted-foreground flex items-center justify-between gap-3"> |
| 70 | <span className="flex items-center gap-2"> | 196 | <span className="flex items-center gap-2"> |
| 71 | <AlertTriangle className="h-4 w-4" /> | 197 | <RefreshCcw className="h-4 w-4" /> |
| 72 | If seat sync fails, use retry once the backend endpoint is available. | 198 | {summary === undefined |
| 199 | ? "Loading billing sync status..." | ||
| 200 | : summary.sync.status === "error" | ||
| 201 | ? "Seat sync needs attention." | ||
| 202 | : summary.sync.status === "ok" | ||
| 203 | ? "Seat sync healthy." | ||
| 204 | : "Seat sync pending. Numbers may be briefly stale during webhook processing."} | ||
| 73 | </span> | 205 | </span> |
| 74 | <Button variant="outline" size="sm" onClick={notReady} disabled={!canManageBilling || actionState === "running"}> | 206 | <Badge variant="outline" className="text-[10px] font-mono uppercase tracking-wider"> |
| 75 | Retry sync | 207 | {summary?.sync.status ?? "pending"} |
| 76 | </Button> | 208 | </Badge> |
| 77 | </CardContent> | 209 | </CardContent> |
| 78 | </Card> | 210 | </Card> |
| 79 | 211 | ||
| 212 | {summary?.sync.error ? ( | ||
| 213 | <Card> | ||
| 214 | <CardContent className="p-4 text-sm text-destructive flex items-center justify-between gap-3"> | ||
| 215 | <span className="flex items-center gap-2"> | ||
| 216 | <AlertTriangle className="h-4 w-4" /> | ||
| 217 | {summary.sync.error} | ||
| 218 | </span> | ||
| 219 | <Button | ||
| 220 | variant="outline" | ||
| 221 | size="sm" | ||
| 222 | onClick={handleRetrySync} | ||
| 223 | disabled={!canManageBilling || actionState === "running"} | ||
| 224 | > | ||
| 225 | Retry sync | ||
| 226 | </Button> | ||
| 227 | </CardContent> | ||
| 228 | </Card> | ||
| 229 | ) : null} | ||
| 230 | |||
| 231 | {!canManageBilling && !organizationsLoading ? ( | ||
| 232 | <Card> | ||
| 233 | <CardContent className="p-4 text-sm text-muted-foreground"> | ||
| 234 | You need owner, admin, or billing admin access to start checkout and open the billing portal. | ||
| 235 | </CardContent> | ||
| 236 | </Card> | ||
| 237 | ) : null} | ||
| 238 | |||
| 80 | <div className="grid gap-4 md:grid-cols-3"> | 239 | <div className="grid gap-4 md:grid-cols-3"> |
| 81 | <Card> | 240 | <Card> |
| 82 | <CardHeader> | 241 | <CardHeader> |
| 83 | <CardTitle className="text-sm">Subscription</CardTitle> | 242 | <CardTitle className="text-sm">Subscription</CardTitle> |
| 84 | </CardHeader> | 243 | </CardHeader> |
| 85 | <CardContent className="space-y-2 text-sm"> | 244 | <CardContent className="space-y-2 text-sm"> |
| 86 | <p>Status: pending</p> | 245 | <p>Status: {summary?.subscription?.status ?? "none"}</p> |
| 87 | <p>Price: -</p> | 246 | <p>Price: {summary?.subscription?.stripePriceId ?? "-"}</p> |
| 88 | <p>Renewal: -</p> | 247 | <p>Renewal: {formatTimestamp(summary?.subscription?.currentPeriodEnd ?? null)}</p> |
| 89 | </CardContent> | 248 | </CardContent> |
| 90 | </Card> | 249 | </Card> |
| 91 | 250 | ||
| @@ -94,9 +253,9 @@ export function BillingView({ showHeader = true }: BillingViewProps) { · 2 unmodified lines | |||
| 94 | <CardTitle className="text-sm">Seats</CardTitle> | 253 | <CardTitle className="text-sm">Seats</CardTitle> |
| 95 | </CardHeader> | 254 | </CardHeader> |
| 96 | <CardContent className="space-y-2 text-sm"> | 255 | <CardContent className="space-y-2 text-sm"> |
| 97 | <p>Billable members: -</p> | 256 | <p>Billable members: {summary?.seats.billableMembers ?? "-"}</p> |
| 98 | <p>Desired seats: -</p> | 257 | <p>Desired seats: {summary?.seats.desiredSeats ?? "-"}</p> |
| 99 | <p>Last applied: -</p> | 258 | <p>Last applied: {summary?.seats.lastAppliedSeats ?? "-"}</p> |
| 100 | </CardContent> | 259 | </CardContent> |
| 101 | </Card> | 260 | </Card> |
| 102 | 261 | ||
| @@ -105,11 +264,26 @@ export function BillingView({ showHeader = true }: BillingViewProps) { · 2 unmodified lines | |||
| 105 | <CardTitle className="text-sm">Actions</CardTitle> | 264 | <CardTitle className="text-sm">Actions</CardTitle> |
| 106 | </CardHeader> | 265 | </CardHeader> |
| 107 | <CardContent className="space-y-2"> | 266 | <CardContent className="space-y-2"> |
| 108 | <Button className="w-full" onClick={notReady} disabled={!canManageBilling || actionState === "running"}> | 267 | <Input |
| 268 | value={priceId} | ||
| 269 | onChange={(event) => setPriceId(event.target.value)} | ||
| 270 | placeholder="price_..." | ||
| 271 | disabled={!canManageBilling || actionState === "running"} | ||
| 272 | /> | ||
| 273 | <Button | ||
| 274 | className="w-full" | ||
| 275 | onClick={handleStartCheckout} | ||
| 276 | disabled={!canManageBilling || actionState === "running"} | ||
| 277 | > | ||
| 109 | <CreditCard className="mr-2 h-4 w-4" /> | 278 | <CreditCard className="mr-2 h-4 w-4" /> |
| 110 | Start checkout | 279 | Start checkout |
| 111 | </Button> | 280 | </Button> |
| 112 | <Button variant="outline" className="w-full" onClick={notReady} disabled={!canManageBilling || actionState === "running"}> | 281 | <Button |
| 282 | variant="outline" | ||
| 283 | className="w-full" | ||
| 284 | onClick={handleOpenPortal} | ||
| 285 | disabled={!canManageBilling || actionState === "running"} | ||
| 286 | > | ||
| 113 | <BadgeCheck className="mr-2 h-4 w-4" /> | 287 | <BadgeCheck className="mr-2 h-4 w-4" /> |
| 114 | Manage billing portal | 288 | Manage billing portal |
| 115 | </Button> | 289 | </Button> |
| @@ -117,8 +291,23 @@ export function BillingView({ showHeader = true }: BillingViewProps) { · 1 unmodified line | |||
| 117 | </Card> | 291 | </Card> |
| 118 | </div> | 292 | </div> |
| 119 | 293 | ||
| 294 | <Card> | ||
| 295 | <CardContent className="p-4 text-sm text-muted-foreground flex items-center gap-2"> | ||
| 296 | <BadgeCheck className="h-4 w-4" /> | ||
| 297 | Customer: {summary?.customer?.stripeCustomerId ?? "No Stripe customer linked"} | ||
| 298 | </CardContent> | ||
| 299 | </Card> | ||
| 300 | |||
| 120 | {actionMessage ? ( | 301 | {actionMessage ? ( |
| 121 | <p className={actionState === "error" ? "text-sm text-destructive" : "text-sm text-muted-foreground"}> | 302 | <p |
| 303 | className={ | ||
| 304 | actionState === "error" | ||
| 305 | ? "text-sm text-destructive" | ||
| 306 | : actionState === "success" | ||
| 307 | ? "text-sm text-terminal-green" | ||
| 308 | : "text-sm text-muted-foreground" | ||
| 309 | } | ||
| 310 | > | ||
| 122 | {actionMessage} | 311 | {actionMessage} |
| 123 | </p> | 312 | </p> |
| 124 | ) : null} | 313 | ) : null} |
| @@ -3,6 +3,7 @@ · 2 unmodified lines | |||
| 3 | import { useRef, useEffect } from "react"; | 3 | import { useRef, useEffect } from "react"; |
| 4 | import Editor, { type OnMount, type BeforeMount, type Monaco } from "@monaco-editor/react"; | 4 | import Editor, { type OnMount, type BeforeMount, type Monaco } from "@monaco-editor/react"; |
| 5 | import { Loader2 } from "lucide-react"; | 5 | import { Loader2 } from "lucide-react"; |
| 6 | import { useTheme } from "next-themes"; | ||
| 6 | import type { ToolDescriptor } from "@/lib/types"; | 7 | import type { ToolDescriptor } from "@/lib/types"; |
| 7 | import { cn } from "@/lib/utils"; | 8 | import { cn } from "@/lib/utils"; |
| 8 | 9 | ||
| @@ -34,7 +35,7 @@ function buildTree(tools: ToolDescriptor[]): NamespaceNode { · 25 unmodified lines | |||
| 34 | return root; | 35 | return root; |
| 35 | } | 36 | } |
| 36 | 37 | ||
| 37 | function emitToolMethod(tool: ToolDescriptor): string { | 38 | function emitToolMethod(tool: ToolDescriptor, dtsSources: Set<string>): string { |
| 38 | const funcName = tool.path.split(".").pop()!; | 39 | const funcName = tool.path.split(".").pop()!; |
| 39 | const approvalNote = | 40 | const approvalNote = |
| 40 | tool.approval === "required" | 41 | tool.approval === "required" |
| @@ -44,8 +45,10 @@ function emitToolMethod(tool: ToolDescriptor): string { · 3 unmodified lines | |||
| 44 | ? `${tool.description}${approvalNote}` | 45 | ? `${tool.description}${approvalNote}` |
| 45 | : approvalNote || "Call this tool."; | 46 | : approvalNote || "Call this tool."; |
| 46 | 47 | ||
| 47 | // For OpenAPI tools with operationId, use indexed access types for precise IntelliSense | 48 | // For OpenAPI tools with operationId, use indexed access types only when |
| 48 | if (tool.operationId) { | 49 | // this source has a loaded .d.ts block. |
| 50 | const hasSourceDts = Boolean(tool.source && dtsSources.has(tool.source)); | ||
| 51 | if (tool.operationId && hasSourceDts) { | ||
| 49 | const opKey = JSON.stringify(tool.operationId); | 52 | const opKey = JSON.stringify(tool.operationId); |
| 50 | return ` /** | 53 | return ` /** |
| 51 | * ${desc} | 54 | * ${desc} |
| @@ -72,11 +75,12 @@ function emitToolMethod(tool: ToolDescriptor): string { · 20 unmodified lines | |||
| 72 | function emitNamespaceInterface( | 75 | function emitNamespaceInterface( |
| 73 | name: string, | 76 | name: string, |
| 74 | node: NamespaceNode, | 77 | node: NamespaceNode, |
| 78 | dtsSources: Set<string>, | ||
| 75 | out: string[], | 79 | out: string[], |
| 76 | ): void { | 80 | ): void { |
| 77 | // First, recursively emit child namespace interfaces | 81 | // First, recursively emit child namespace interfaces |
| 78 | for (const [childName, childNode] of node.children) { | 82 | for (const [childName, childNode] of node.children) { |
| 79 | emitNamespaceInterface(`${name}_${childName}`, childNode, out); | 83 | emitNamespaceInterface(`${name}_${childName}`, childNode, dtsSources, out); |
| 80 | } | 84 | } |
| 81 | 85 | ||
| 82 | // Build this namespace's interface | 86 | // Build this namespace's interface |
| @@ -92,7 +96,7 @@ function emitNamespaceInterface( · 9 unmodified lines | |||
| 92 | 96 | ||
| 93 | // Add tool methods | 97 | // Add tool methods |
| 94 | for (const tool of node.tools) { | 98 | for (const tool of node.tools) { |
| 95 | members.push(emitToolMethod(tool)); | 99 | members.push(emitToolMethod(tool, dtsSources)); |
| 96 | } | 100 | } |
| 97 | 101 | ||
| 98 | out.push(`interface ToolNS_${name} {\n${members.join("\n\n")}\n}`); | 102 | out.push(`interface ToolNS_${name} {\n${members.join("\n\n")}\n}`); |
| @@ -119,11 +123,12 @@ type ToolOutput<Op> = · 20 unmodified lines | |||
| 119 | Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R : | 123 | Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R : |
| 120 | Op extends { responses: { 201: { content: { "application/json": infer R } } } } ? R : | 124 | Op extends { responses: { 201: { content: { "application/json": infer R } } } } ? R : |
| 121 | Op extends { responses: { 202: { content: { "application/json": infer R } } } } ? R : | 125 | Op extends { responses: { 202: { content: { "application/json": infer R } } } } ? R : |
| 122 | Op extends { responses: { 204: { content: never } } } ? void : | 126 | Op extends { responses: { 204: unknown } } ? void : |
| 127 | Op extends { responses: { 205: unknown } } ? void : | ||
| 123 | unknown; | 128 | unknown; |
| 124 | `; | 129 | `; |
| 125 | 130 | ||
| 126 | function generateToolsDts(tools: ToolDescriptor[]): string { | 131 | function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string { |
| 127 | const root = buildTree(tools); | 132 | const root = buildTree(tools); |
| 128 | 133 | ||
| 129 | // Legacy compat: collect schemaTypes from tools that use the old format | 134 | // Legacy compat: collect schemaTypes from tools that use the old format |
| @@ -141,7 +146,7 @@ function generateToolsDts(tools: ToolDescriptor[]): string { · 11 unmodified lines | |||
| 141 | 146 | ||
| 142 | // Emit all namespace interfaces recursively | 147 | // Emit all namespace interfaces recursively |
| 143 | for (const [name, node] of root.children) { | 148 | for (const [name, node] of root.children) { |
| 144 | emitNamespaceInterface(name, node, interfaces); | 149 | emitNamespaceInterface(name, node, dtsSources, interfaces); |
| 145 | } | 150 | } |
| 146 | 151 | ||
| 147 | // Build root ToolsProxy interface | 152 | // Build root ToolsProxy interface |
| @@ -154,7 +159,7 @@ function generateToolsDts(tools: ToolDescriptor[]): string { · 6 unmodified lines | |||
| 154 | 159 | ||
| 155 | // Root-level tools (rare but possible) | 160 | // Root-level tools (rare but possible) |
| 156 | for (const tool of root.tools) { | 161 | for (const tool of root.tools) { |
| 157 | rootMembers.push(emitToolMethod(tool)); | 162 | rootMembers.push(emitToolMethod(tool, dtsSources)); |
| 158 | } | 163 | } |
| 159 | 164 | ||
| 160 | let dts = ` | 165 | let dts = ` |
| @@ -241,6 +246,7 @@ export function CodeEditor({ · 80 unmodified lines | |||
| 241 | className, | 246 | className, |
| 242 | height = "400px", | 247 | height = "400px", |
| 243 | }: CodeEditorProps) { | 248 | }: CodeEditorProps) { |
| 249 | const { resolvedTheme } = useTheme(); | ||
| 244 | const editorRef = useRef<Parameters<OnMount>[0] | null>(null); | 250 | const editorRef = useRef<Parameters<OnMount>[0] | null>(null); |
| 245 | const monacoRef = useRef<Monaco | null>(null); | 251 | const monacoRef = useRef<Monaco | null>(null); |
| 246 | const envLibDisposable = useRef<{ dispose: () => void } | null>(null); | 252 | const envLibDisposable = useRef<{ dispose: () => void } | null>(null); |
| @@ -248,6 +254,7 @@ export function CodeEditor({ · 1 unmodified line | |||
| 248 | const dtsLibDisposables = useRef<{ dispose: () => void }[]>([]); | 254 | const dtsLibDisposables = useRef<{ dispose: () => void }[]>([]); |
| 249 | const toolsLibVersion = useRef(0); | 255 | const toolsLibVersion = useRef(0); |
| 250 | const fetchedDtsUrls = useRef<string>(""); | 256 | const fetchedDtsUrls = useRef<string>(""); |
| 257 | const dtsSources = new Set(Object.keys(dtsUrls ?? {})); | ||
| 251 | 258 | ||
| 252 | // Fetch and register .d.ts blobs from OpenAPI sources | 259 | // Fetch and register .d.ts blobs from OpenAPI sources |
| 253 | useEffect(() => { | 260 | useEffect(() => { |
| @@ -318,7 +325,7 @@ export function CodeEditor({ · 64 unmodified lines | |||
| 318 | // Dispose previous tool type declarations | 325 | // Dispose previous tool type declarations |
| 319 | toolsLibDisposable.current?.dispose(); | 326 | toolsLibDisposable.current?.dispose(); |
| 320 | 327 | ||
| 321 | const dts = generateToolsDts(tools); | 328 | const dts = generateToolsDts(tools, dtsSources); |
| 322 | 329 | ||
| 323 | // Use a versioned filename — disposing + re-adding the same filename | 330 | // Use a versioned filename — disposing + re-adding the same filename |
| 324 | // can cause the TS worker to serve stale completions from its cache. | 331 | // can cause the TS worker to serve stale completions from its cache. |
| @@ -327,7 +334,7 @@ export function CodeEditor({ · 2 unmodified lines | |||
| 327 | dts, | 334 | dts, |
| 328 | `file:///node_modules/@types/executor-tools/v${version}.d.ts`, | 335 | `file:///node_modules/@types/executor-tools/v${version}.d.ts`, |
| 329 | ); | 336 | ); |
| 330 | }, [tools]); | 337 | }, [tools, dtsUrls]); |
| 331 | 338 | ||
| 332 | // Avoid transient semantic errors while tool metadata is still loading. | 339 | // Avoid transient semantic errors while tool metadata is still loading. |
| 333 | useEffect(() => { | 340 | useEffect(() => { |
| @@ -382,14 +389,62 @@ export function CodeEditor({ · 48 unmodified lines | |||
| 382 | // Add initial tool type declarations | 389 | // Add initial tool type declarations |
| 383 | // (will be replaced by useEffect when tools load from the API) | 390 | // (will be replaced by useEffect when tools load from the API) |
| 384 | toolsLibDisposable.current?.dispose(); | 391 | toolsLibDisposable.current?.dispose(); |
| 385 | const dts = generateToolsDts(tools); | 392 | const dts = generateToolsDts(tools, dtsSources); |
| 386 | const version = ++toolsLibVersion.current; | 393 | const version = ++toolsLibVersion.current; |
| 387 | toolsLibDisposable.current = ts.javascriptDefaults.addExtraLib( | 394 | toolsLibDisposable.current = ts.javascriptDefaults.addExtraLib( |
| 388 | dts, | 395 | dts, |
| 389 | `file:///node_modules/@types/executor-tools/v${version}.d.ts`, | 396 | `file:///node_modules/@types/executor-tools/v${version}.d.ts`, |
| 390 | ); | 397 | ); |
| 391 | 398 | ||
| 392 | // Define the dark theme matching our UI | 399 | // Define themes that track the app's light/dark mode. |
| 400 | monaco.editor.defineTheme("executor-light", { | ||
| 401 | base: "vs", | ||
| 402 | inherit: true, | ||
| 403 | rules: [ | ||
| 404 | { token: "comment", foreground: "7f8692", fontStyle: "italic" }, | ||
| 405 | { token: "keyword", foreground: "0f8a6a" }, | ||
| 406 | { token: "string", foreground: "a46822" }, | ||
| 407 | { token: "number", foreground: "a46822" }, | ||
| 408 | { token: "type", foreground: "0f8a6a" }, | ||
| 409 | { token: "function", foreground: "5c470f" }, | ||
| 410 | { token: "variable", foreground: "1f2430" }, | ||
| 411 | { token: "operator", foreground: "6f7785" }, | ||
| 412 | ], | ||
| 413 | colors: { | ||
| 414 | "editor.background": "#ffffff", | ||
| 415 | "editor.foreground": "#1f2430", | ||
| 416 | "editor.lineHighlightBackground": "#f4f7fb", | ||
| 417 | "editor.selectionBackground": "#c7def5", | ||
| 418 | "editor.inactiveSelectionBackground": "#dbe8f7", | ||
| 419 | "editorCursor.foreground": "#0f8a6a", | ||
| 420 | "editorLineNumber.foreground": "#9aa3b2", | ||
| 421 | "editorLineNumber.activeForeground": "#6f7785", | ||
| 422 | "editorIndentGuide.background": "#e3e8ef", | ||
| 423 | "editorIndentGuide.activeBackground": "#ccd5e2", | ||
| 424 | "editor.selectionHighlightBackground": "#c7def540", | ||
| 425 | "editorWidget.background": "#ffffff", | ||
| 426 | "editorWidget.border": "#d6dde8", | ||
| 427 | "editorSuggestWidget.background": "#ffffff", | ||
| 428 | "editorSuggestWidget.border": "#d6dde8", | ||
| 429 | "editorSuggestWidget.selectedBackground": "#cfe1f6", | ||
| 430 | "editorSuggestWidget.selectedForeground": "#111827", | ||
| 431 | "editorSuggestWidget.selectedIconForeground": "#0f8a6a", | ||
| 432 | "editorSuggestWidget.highlightForeground": "#0f8a6a", | ||
| 433 | "editorHoverWidget.background": "#ffffff", | ||
| 434 | "editorHoverWidget.border": "#d6dde8", | ||
| 435 | "list.focusBackground": "#cfe1f6", | ||
| 436 | "list.focusForeground": "#111827", | ||
| 437 | "list.highlightForeground": "#0f8a6a", | ||
| 438 | "input.background": "#ffffff", | ||
| 439 | "input.border": "#d6dde8", | ||
| 440 | "scrollbar.shadow": "#00000000", | ||
| 441 | "scrollbarSlider.background": "#c2cad880", | ||
| 442 | "scrollbarSlider.hoverBackground": "#a6afbe", | ||
| 443 | "scrollbarSlider.activeBackground": "#8f98a8", | ||
| 444 | "focusBorder": "#0f8a6a30", | ||
| 445 | }, | ||
| 446 | }); | ||
| 447 | |||
| 393 | monaco.editor.defineTheme("executor-dark", { | 448 | monaco.editor.defineTheme("executor-dark", { |
| 394 | base: "vs-dark", | 449 | base: "vs-dark", |
| 395 | inherit: true, | 450 | inherit: true, |
| @@ -434,6 +489,8 @@ export function CodeEditor({ · 38 unmodified lines | |||
| 434 | }); | 489 | }); |
| 435 | }; | 490 | }; |
| 436 | 491 | ||
| 492 | const monacoTheme = resolvedTheme === "light" ? "executor-light" : "executor-dark"; | ||
| 493 | |||
| 437 | const handleMount: OnMount = (editor) => { | 494 | const handleMount: OnMount = (editor) => { |
| 438 | editorRef.current = editor; | 495 | editorRef.current = editor; |
| 439 | 496 | ||
| @@ -460,7 +517,7 @@ export function CodeEditor({ · 20 unmodified lines | |||
| 460 | height={height} | 517 | height={height} |
| 461 | language="javascript" | 518 | language="javascript" |
| 462 | path="task.js" | 519 | path="task.js" |
| 463 | theme="executor-dark" | 520 | theme={monacoTheme} |
| 464 | value={value} | 521 | value={value} |
| 465 | onChange={(v) => onChange(v ?? "")} | 522 | onChange={(v) => onChange(v ?? "")} |
| 466 | beforeMount={handleBeforeMount} | 523 | beforeMount={handleBeforeMount} |
| @@ -524,7 +581,7 @@ export function CodeEditor({ · 57 unmodified lines | |||
| 524 | smoothScrolling: true, | 581 | smoothScrolling: true, |
| 525 | }} | 582 | }} |
| 526 | loading={ | 583 | loading={ |
| 527 | <div className="flex items-center justify-center h-full bg-[#0f1117] text-muted-foreground text-xs font-mono"> | 584 | <div className="flex h-full items-center justify-center bg-background text-xs font-mono text-muted-foreground"> |
| 528 | Loading editor... | 585 | Loading editor... |
| 529 | </div> | 586 | </div> |
| 530 | } | 587 | } |
| @@ -256,6 +256,93 @@ function CopyButton({ · 255 unmodified lines | |||
| 256 | ); | 256 | ); |
| 257 | } | 257 | } |
| 258 | 258 | ||
| 259 | // ── Pretty-print a TS type literal for display ── | ||
| 260 | // Turns `{ owner: string; repo: string; ref?: string }` into a multi-line | ||
| 261 | // indented format. Handles nested objects/arrays. Pure string transform. | ||
| 262 | |||
| 263 | function prettyType(raw: string): string { | ||
| 264 | const trimmed = raw.trim(); | ||
| 265 | // If it's already multi-line or short enough, leave it | ||
| 266 | if (trimmed.includes("\n") || trimmed.length < 50) return trimmed; | ||
| 267 | |||
| 268 | let out = ""; | ||
| 269 | let indent = 0; | ||
| 270 | const TAB = " "; | ||
| 271 | let i = 0; | ||
| 272 | |||
| 273 | while (i < trimmed.length) { | ||
| 274 | const ch = trimmed[i]; | ||
| 275 | |||
| 276 | // Array type bracket — e.g. `string[]` or `}[]` — keep inline | ||
| 277 | if (ch === "[" && trimmed[i + 1] === "]") { | ||
| 278 | out += "[]"; | ||
| 279 | i += 2; | ||
| 280 | } else if (ch === "{") { | ||
| 281 | indent++; | ||
| 282 | out += ch + "\n" + TAB.repeat(indent); | ||
| 283 | i++; | ||
| 284 | // skip whitespace after brace | ||
| 285 | while (i < trimmed.length && trimmed[i] === " ") i++; | ||
| 286 | } else if (ch === "}") { | ||
| 287 | indent = Math.max(0, indent - 1); | ||
| 288 | // trim trailing whitespace on current line | ||
| 289 | out = out.replace(/\s+$/, ""); | ||
| 290 | out += "\n" + TAB.repeat(indent) + ch; | ||
| 291 | i++; | ||
| 292 | } else if (ch === ";" && trimmed[i + 1] === " ") { | ||
| 293 | // property separator — newline after semicolon | ||
| 294 | out += ";\n" + TAB.repeat(indent); | ||
| 295 | i += 2; // skip the semicolon + space | ||
| 296 | } else if (ch === "," && indent > 0 && trimmed[i + 1] === " ") { | ||
| 297 | out += ",\n" + TAB.repeat(indent); | ||
| 298 | i += 2; | ||
| 299 | } else { | ||
| 300 | out += ch; | ||
| 301 | i++; | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | return out; | ||
| 306 | } | ||
| 307 | |||
| 308 | // ── Formatted + highlighted type signature display ── | ||
| 309 | |||
| 310 | function TypeSignature({ raw, label }: { raw: string; label: string }) { | ||
| 311 | const formatted = useMemo(() => prettyType(raw), [raw]); | ||
| 312 | const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null); | ||
| 313 | |||
| 314 | useEffect(() => { | ||
| 315 | let cancelled = false; | ||
| 316 | import("shiki").then(({ codeToHtml }) => | ||
| 317 | codeToHtml(formatted, { | ||
| 318 | lang: "typescript", | ||
| 319 | themes: { light: "github-light", dark: "github-dark" }, | ||
| 320 | }).then((html) => { | ||
| 321 | if (!cancelled) setHighlightedHtml(html); | ||
| 322 | }), | ||
| 323 | ); | ||
| 324 | return () => { cancelled = true; }; | ||
| 325 | }, [formatted]); | ||
| 326 | |||
| 327 | return ( | ||
| 328 | <div> | ||
| 329 | <p className="text-[9px] font-mono uppercase tracking-wider text-muted-foreground/50 mb-1"> | ||
| 330 | {label} | ||
| 331 | </p> | ||
| 332 | {highlightedHtml ? ( | ||
| 333 | <div | ||
| 334 | className="type-signature text-[11px] leading-relaxed bg-muted/40 border border-border/40 rounded-md px-2.5 py-2 overflow-x-auto [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_code]:!text-[11px] [&_code]:!leading-relaxed" | ||
| 335 | dangerouslySetInnerHTML={{ __html: highlightedHtml }} | ||
| 336 | /> | ||
| 337 | ) : ( | ||
| 338 | <pre className="text-[11px] font-mono leading-relaxed text-foreground/80 bg-muted/40 border border-border/40 rounded-md px-2.5 py-2 overflow-x-auto whitespace-pre"> | ||
| 339 | {formatted} | ||
| 340 | </pre> | ||
| 341 | )} | ||
| 342 | </div> | ||
| 343 | ); | ||
| 344 | } | ||
| 345 | |||
| 259 | // ── Inline tool detail (shown when tool row is expanded) ── | 346 | // ── Inline tool detail (shown when tool row is expanded) ── |
| 260 | 347 | ||
| 261 | function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) { | 348 | function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) { |
| @@ -289,26 +376,12 @@ function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) { · 27 unmodified lines | |||
| 289 | )} | 376 | )} |
| 290 | </div> | 377 | </div> |
| 291 | 378 | ||
| 292 | {/* Args / Returns rendered as highlighted TypeScript via Streamdown */} | 379 | {/* Args / Returns — pretty-printed type signatures */} |
| 293 | {tool.argsType && ( | 380 | {tool.argsType && ( |
| 294 | <div className="tool-description"> | 381 | <TypeSignature raw={tool.argsType} label="Arguments" /> |
| 295 | <p className="text-[9px] font-mono uppercase tracking-wider text-muted-foreground/50 mb-1"> | ||
| 296 | Arguments | ||
| 297 | </p> | ||
| 298 | <Streamdown plugins={{ code: codePlugin }}> | ||
| 299 | {"```ts\n" + tool.argsType + "\n```"} | ||
| 300 | </Streamdown> | ||
| 301 | </div> | ||
| 302 | )} | 382 | )} |
| 303 | {tool.returnsType && ( | 383 | {tool.returnsType && ( |
| 304 | <div className="tool-description"> | 384 | <TypeSignature raw={tool.returnsType} label="Returns" /> |
| 305 | <p className="text-[9px] font-mono uppercase tracking-wider text-muted-foreground/50 mb-1"> | ||
| 306 | Returns | ||
| 307 | </p> | ||
| 308 | <Streamdown plugins={{ code: codePlugin }}> | ||
| 309 | {"```ts\n" + tool.returnsType + "\n```"} | ||
| 310 | </Streamdown> | ||
| 311 | </div> | ||
| 312 | )} | 385 | )} |
| 313 | </div> | 386 | </div> |
| 314 | ); | 387 | ); |
| @@ -207,18 +207,46 @@ function faviconForUrl(url: string | undefined | null): string | null { · 206 unmodified lines | |||
| 207 | } | 207 | } |
| 208 | } | 208 | } |
| 209 | 209 | ||
| 210 | function isTemplateHostname(hostname: string): boolean { | ||
| 211 | const lower = hostname.toLowerCase(); | ||
| 212 | return ( | ||
| 213 | lower === "localhost" || | ||
| 214 | lower === "example.com" || | ||
| 215 | lower.endsWith(".example.com") || | ||
| 216 | lower.startsWith("your-domain") || | ||
| 217 | lower.includes(".your-domain") | ||
| 218 | ); | ||
| 219 | } | ||
| 220 | |||
| 221 | function pickFaviconTarget(urls: Array<string | undefined | null>): string | null { | ||
| 222 | const parsed = urls | ||
| 223 | .filter((value): value is string => Boolean(value)) | ||
| 224 | .map((value) => { | ||
| 225 | try { | ||
| 226 | return { value, hostname: new URL(value).hostname }; | ||
| 227 | } catch { | ||
| 228 | return null; | ||
| 229 | } | ||
| 230 | }) | ||
| 231 | .filter((entry): entry is { value: string; hostname: string } => entry !== null); | ||
| 232 | |||
| 233 | const preferred = parsed.find((entry) => !isTemplateHostname(entry.hostname)); | ||
| 234 | if (preferred) return preferred.value; | ||
| 235 | return parsed[0]?.value ?? null; | ||
| 236 | } | ||
| 237 | |||
| 210 | function getFaviconUrl(preset: ApiPreset): string | null { | 238 | function getFaviconUrl(preset: ApiPreset): string | null { |
| 211 | return faviconForUrl(preset.baseUrl ?? preset.endpoint ?? preset.url); | 239 | return faviconForUrl(pickFaviconTarget([preset.baseUrl, preset.endpoint, preset.url, preset.spec])); |
| 212 | } | 240 | } |
| 213 | 241 | ||
| 214 | function getSourceFavicon(source: ToolSourceRecord): string | null { | 242 | function getSourceFavicon(source: ToolSourceRecord): string | null { |
| 215 | const url = | 243 | const urls = |
| 216 | source.type === "mcp" | 244 | source.type === "mcp" |
| 217 | ? (source.config.url as string) | 245 | ? [source.config.url as string] |
| 218 | : source.type === "graphql" | 246 | : source.type === "graphql" |
| 219 | ? (source.config.endpoint as string) | 247 | ? [source.config.endpoint as string] |
| 220 | : (source.config.baseUrl as string) ?? (source.config.spec as string); | 248 | : [source.config.baseUrl as string, source.config.spec as string]; |
| 221 | return faviconForUrl(url); | 249 | return faviconForUrl(pickFaviconTarget(urls)); |
| 222 | } | 250 | } |
| 223 | 251 | ||
| 224 | function sourceKeyForSource(source: ToolSourceRecord): string | null { | 252 | function sourceKeyForSource(source: ToolSourceRecord): string | null { |
| @@ -1,3 +1,6 @@ | |||
| 1 | { | 1 | { |
| 2 | "functions": "convex" | 2 | "functions": "convex", |
| 3 | "codegen": { | ||
| 4 | "staticApi": true | ||
| 5 | } | ||
| 3 | } | 6 | } |
| @@ -5,15 +5,11 @@ · 4 unmodified lines | |||
| 5 | * THIS CODE IS AUTOMATICALLY GENERATED. | 5 | * THIS CODE IS AUTOMATICALLY GENERATED. |
| 6 | * | 6 | * |
| 7 | * To regenerate, run `npx convex dev`. | 7 | * To regenerate, run `npx convex dev`. |
| 8 | * | ||
| 9 | * NOTE: This file has been post-processed by scripts/postcodegen.ts | ||
| 10 | * to replace recursive ApiFromModules/FilterApi types with explicit | ||
| 11 | * declarations, avoiding TS2589 depth errors. | ||
| 12 | * | ||
| 13 | * @module | 8 | * @module |
| 14 | */ | 9 | */ |
| 15 | 10 | ||
| 16 | import type { FunctionReference } from "convex/server"; | 11 | import type { FunctionReference } from "convex/server"; |
| 12 | import type { GenericId as Id } from "convex/values"; | ||
| 17 | 13 | ||
| 18 | /** | 14 | /** |
| 19 | * A utility for referencing Convex functions in your app's public API. | 15 | * A utility for referencing Convex functions in your app's public API. |
| @@ -25,75 +21,413 @@ import type { FunctionReference } from "convex/server"; · 5 unmodified lines | |||
| 25 | */ | 21 | */ |
| 26 | export declare const api: { | 22 | export declare const api: { |
| 27 | app: { | 23 | app: { |
| 28 | getClientConfig: FunctionReference<"query", "public", any, any>; | 24 | getClientConfig: FunctionReference<"query", "public", {}, any>; |
| 29 | getCurrentAccount: FunctionReference<"query", "public", any, any>; | 25 | getCurrentAccount: FunctionReference< |
| 26 | "query", | ||
| 27 | "public", | ||
| 28 | { sessionId?: string }, | ||
| 29 | any | ||
| 30 | >; | ||
| 30 | }; | 31 | }; |
| 31 | auth: { | 32 | auth: { |
| 32 | bootstrapCurrentWorkosAccount: FunctionReference<"mutation", "public", any, any>; | 33 | bootstrapCurrentWorkosAccount: FunctionReference< |
| 34 | "mutation", | ||
| 35 | "public", | ||
| 36 | {}, | ||
| 37 | any | ||
| 38 | >; | ||
| 33 | }; | 39 | }; |
| 34 | billing: { | 40 | billing: { |
| 35 | getSummary: FunctionReference<"query", "public", any, any>; | 41 | createCustomerPortal: FunctionReference< |
| 36 | createSubscriptionCheckout: FunctionReference<"action", "public", any, any>; | 42 | "action", |
| 37 | createCustomerPortal: FunctionReference<"action", "public", any, any>; | 43 | "public", |
| 38 | retrySeatSync: FunctionReference<"mutation", "public", any, any>; | 44 | { |
| 45 | organizationId: Id<"organizations">; | ||
| 46 | returnUrl?: string; | ||
| 47 | sessionId?: string; | ||
| 48 | }, | ||
| 49 | { url: string } | ||
| 50 | >; | ||
| 51 | createSubscriptionCheckout: FunctionReference< | ||
| 52 | "action", | ||
| 53 | "public", | ||
| 54 | { | ||
| 55 | cancelUrl?: string; | ||
| 56 | organizationId: Id<"organizations">; | ||
| 57 | priceId: string; | ||
| 58 | sessionId?: string; | ||
| 59 | successUrl?: string; | ||
| 60 | }, | ||
| 61 | any | ||
| 62 | >; | ||
| 63 | getSummary: FunctionReference< | ||
| 64 | "query", | ||
| 65 | "public", | ||
| 66 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 67 | any | ||
| 68 | >; | ||
| 69 | retrySeatSync: FunctionReference< | ||
| 70 | "mutation", | ||
| 71 | "public", | ||
| 72 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 73 | any | ||
| 74 | >; | ||
| 39 | }; | 75 | }; |
| 40 | credentialsNode: { | 76 | credentialsNode: { |
| 41 | upsertCredential: FunctionReference<"action", "public", any, any>; | 77 | upsertCredential: FunctionReference< |
| 78 | "action", | ||
| 79 | "public", | ||
| 80 | { | ||
| 81 | actorId?: string; | ||
| 82 | id?: string; | ||
| 83 | provider?: "managed" | "workos-vault"; | ||
| 84 | scope: "workspace" | "actor"; | ||
| 85 | secretJson: any; | ||
| 86 | sessionId?: string; | ||
| 87 | sourceKey: string; | ||
| 88 | workspaceId: string; | ||
| 89 | }, | ||
| 90 | any | ||
| 91 | >; | ||
| 42 | }; | 92 | }; |
| 43 | database: { | 93 | database: { |
| 44 | createAgentTask: FunctionReference<"mutation", "public", any, any>; | 94 | createAgentTask: FunctionReference< |
| 45 | getAgentTask: FunctionReference<"query", "public", any, any>; | 95 | "mutation", |
| 46 | updateAgentTask: FunctionReference<"mutation", "public", any, any>; | 96 | "public", |
| 97 | { | ||
| 98 | actorId: string; | ||
| 99 | id: string; | ||
| 100 | prompt: string; | ||
| 101 | requesterId: string; | ||
| 102 | workspaceId: string; | ||
| 103 | }, | ||
| 104 | any | ||
| 105 | >; | ||
| 106 | getAgentTask: FunctionReference< | ||
| 107 | "query", | ||
| 108 | "public", | ||
| 109 | { agentTaskId: string }, | ||
| 110 | any | ||
| 111 | >; | ||
| 112 | updateAgentTask: FunctionReference< | ||
| 113 | "mutation", | ||
| 114 | "public", | ||
| 115 | { | ||
| 116 | agentTaskId: string; | ||
| 117 | codeRuns?: number; | ||
| 118 | error?: string; | ||
| 119 | resultText?: string; | ||
| 120 | status?: "running" | "completed" | "failed"; | ||
| 121 | }, | ||
| 122 | any | ||
| 123 | >; | ||
| 47 | }; | 124 | }; |
| 48 | executor: { | 125 | executor: { |
| 49 | createTask: FunctionReference<"mutation", "public", any, any>; | 126 | createTask: FunctionReference< |
| 50 | resolveApproval: FunctionReference<"mutation", "public", any, any>; | 127 | "mutation", |
| 128 | "public", | ||
| 129 | { | ||
| 130 | actorId?: string; | ||
| 131 | clientId?: string; | ||
| 132 | code: string; | ||
| 133 | metadata?: any; | ||
| 134 | runtimeId?: string; | ||
| 135 | sessionId?: string; | ||
| 136 | timeoutMs?: number; | ||
| 137 | workspaceId: string; | ||
| 138 | }, | ||
| 139 | any | ||
| 140 | >; | ||
| 141 | resolveApproval: FunctionReference< | ||
| 142 | "mutation", | ||
| 143 | "public", | ||
| 144 | { | ||
| 145 | approvalId: string; | ||
| 146 | decision: "approved" | "denied"; | ||
| 147 | reason?: string; | ||
| 148 | reviewerId?: string; | ||
| 149 | sessionId?: string; | ||
| 150 | workspaceId: string; | ||
| 151 | }, | ||
| 152 | any | ||
| 153 | >; | ||
| 51 | }; | 154 | }; |
| 52 | executorNode: { | 155 | executorNode: { |
| 53 | listTools: FunctionReference<"action", "public", any, any>; | 156 | listTools: FunctionReference< |
| 54 | listToolsWithWarnings: FunctionReference<"action", "public", any, any>; | 157 | "action", |
| 158 | "public", | ||
| 159 | { | ||
| 160 | actorId?: string; | ||
| 161 | clientId?: string; | ||
| 162 | sessionId?: string; | ||
| 163 | workspaceId: string; | ||
| 164 | }, | ||
| 165 | any | ||
| 166 | >; | ||
| 167 | listToolsWithWarnings: FunctionReference< | ||
| 168 | "action", | ||
| 169 | "public", | ||
| 170 | { | ||
| 171 | actorId?: string; | ||
| 172 | clientId?: string; | ||
| 173 | sessionId?: string; | ||
| 174 | workspaceId: string; | ||
| 175 | }, | ||
| 176 | any | ||
| 177 | >; | ||
| 55 | }; | 178 | }; |
| 56 | invites: { | 179 | invites: { |
| 57 | list: FunctionReference<"query", "public", any, any>; | 180 | create: FunctionReference< |
| 58 | create: FunctionReference<"mutation", "public", any, any>; | 181 | "mutation", |
| 59 | revoke: FunctionReference<"mutation", "public", any, any>; | 182 | "public", |
| 183 | { | ||
| 184 | email: string; | ||
| 185 | expiresInDays?: number; | ||
| 186 | organizationId: Id<"organizations">; | ||
| 187 | role: "owner" | "admin" | "member" | "billing_admin"; | ||
| 188 | sessionId?: string; | ||
| 189 | workspaceId?: Id<"workspaces">; | ||
| 190 | }, | ||
| 191 | any | ||
| 192 | >; | ||
| 193 | list: FunctionReference< | ||
| 194 | "query", | ||
| 195 | "public", | ||
| 196 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 197 | any | ||
| 198 | >; | ||
| 199 | revoke: FunctionReference< | ||
| 200 | "mutation", | ||
| 201 | "public", | ||
| 202 | { | ||
| 203 | inviteId: Id<"invites">; | ||
| 204 | organizationId: Id<"organizations">; | ||
| 205 | sessionId?: string; | ||
| 206 | }, | ||
| 207 | any | ||
| 208 | >; | ||
| 60 | }; | 209 | }; |
| 61 | organizationMembers: { | 210 | organizationMembers: { |
| 62 | list: FunctionReference<"query", "public", any, any>; | 211 | list: FunctionReference< |
| 63 | updateRole: FunctionReference<"mutation", "public", any, any>; | 212 | "query", |
| 64 | updateBillable: FunctionReference<"mutation", "public", any, any>; | 213 | "public", |
| 65 | remove: FunctionReference<"mutation", "public", any, any>; | 214 | { organizationId: Id<"organizations">; sessionId?: string }, |
| 215 | any | ||
| 216 | >; | ||
| 217 | remove: FunctionReference< | ||
| 218 | "mutation", | ||
| 219 | "public", | ||
| 220 | { | ||
| 221 | accountId: Id<"accounts">; | ||
| 222 | organizationId: Id<"organizations">; | ||
| 223 | sessionId?: string; | ||
| 224 | }, | ||
| 225 | any | ||
| 226 | >; | ||
| 227 | updateBillable: FunctionReference< | ||
| 228 | "mutation", | ||
| 229 | "public", | ||
| 230 | { | ||
| 231 | accountId: Id<"accounts">; | ||
| 232 | billable: boolean; | ||
| 233 | organizationId: Id<"organizations">; | ||
| 234 | sessionId?: string; | ||
| 235 | }, | ||
| 236 | any | ||
| 237 | >; | ||
| 238 | updateRole: FunctionReference< | ||
| 239 | "mutation", | ||
| 240 | "public", | ||
| 241 | { | ||
| 242 | accountId: Id<"accounts">; | ||
| 243 | organizationId: Id<"organizations">; | ||
| 244 | role: "owner" | "admin" | "member" | "billing_admin"; | ||
| 245 | sessionId?: string; | ||
| 246 | }, | ||
| 247 | any | ||
| 248 | >; | ||
| 66 | }; | 249 | }; |
| 67 | organizations: { | 250 | organizations: { |
| 68 | create: FunctionReference<"mutation", "public", any, any>; | 251 | create: FunctionReference< |
| 69 | listMine: FunctionReference<"query", "public", any, any>; | 252 | "mutation", |
| 70 | getNavigationState: FunctionReference<"query", "public", any, any>; | 253 | "public", |
| 71 | getOrganizationAccess: FunctionReference<"query", "public", any, any>; | 254 | { name: string; sessionId?: string }, |
| 72 | resolveWorkosOrganizationId: FunctionReference<"query", "public", any, any>; | 255 | any |
| 256 | >; | ||
| 257 | getNavigationState: FunctionReference< | ||
| 258 | "query", | ||
| 259 | "public", | ||
| 260 | { sessionId?: string }, | ||
| 261 | any | ||
| 262 | >; | ||
| 263 | getOrganizationAccess: FunctionReference< | ||
| 264 | "query", | ||
| 265 | "public", | ||
| 266 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 267 | any | ||
| 268 | >; | ||
| 269 | listMine: FunctionReference<"query", "public", { sessionId?: string }, any>; | ||
| 270 | resolveWorkosOrganizationId: FunctionReference< | ||
| 271 | "query", | ||
| 272 | "public", | ||
| 273 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 274 | any | ||
| 275 | >; | ||
| 73 | }; | 276 | }; |
| 74 | workspace: { | 277 | workspace: { |
| 75 | bootstrapAnonymousSession: FunctionReference<"mutation", "public", any, any>; | 278 | bootstrapAnonymousSession: FunctionReference< |
| 76 | listRuntimeTargets: FunctionReference<"query", "public", any, any>; | 279 | "mutation", |
| 77 | getTask: FunctionReference<"query", "public", any, any>; | 280 | "public", |
| 78 | getTaskInWorkspace: FunctionReference<"query", "public", any, any>; | 281 | { sessionId?: string }, |
| 79 | listTasks: FunctionReference<"query", "public", any, any>; | 282 | any |
| 80 | listApprovals: FunctionReference<"query", "public", any, any>; | 283 | >; |
| 81 | listPendingApprovals: FunctionReference<"query", "public", any, any>; | 284 | deleteToolSource: FunctionReference< |
| 82 | listTaskEvents: FunctionReference<"query", "public", any, any>; | 285 | "mutation", |
| 83 | upsertAccessPolicy: FunctionReference<"mutation", "public", any, any>; | 286 | "public", |
| 84 | listAccessPolicies: FunctionReference<"query", "public", any, any>; | 287 | { sessionId?: string; sourceId: string; workspaceId: string }, |
| 85 | upsertCredential: FunctionReference<"mutation", "public", any, any>; | 288 | any |
| 86 | listCredentials: FunctionReference<"query", "public", any, any>; | 289 | >; |
| 87 | listCredentialProviders: FunctionReference<"query", "public", any, any>; | 290 | getTask: FunctionReference< |
| 88 | resolveCredential: FunctionReference<"query", "public", any, any>; | 291 | "query", |
| 89 | upsertToolSource: FunctionReference<"mutation", "public", any, any>; | 292 | "public", |
| 90 | listToolSources: FunctionReference<"query", "public", any, any>; | 293 | { sessionId?: string; taskId: string; workspaceId: string }, |
| 91 | deleteToolSource: FunctionReference<"mutation", "public", any, any>; | 294 | any |
| 295 | >; | ||
| 296 | getTaskInWorkspace: FunctionReference< | ||
| 297 | "query", | ||
| 298 | "public", | ||
| 299 | { sessionId?: string; taskId: string; workspaceId: string }, | ||
| 300 | any | ||
| 301 | >; | ||
| 302 | listAccessPolicies: FunctionReference< | ||
| 303 | "query", | ||
| 304 | "public", | ||
| 305 | { sessionId?: string; workspaceId: string }, | ||
| 306 | any | ||
| 307 | >; | ||
| 308 | listApprovals: FunctionReference< | ||
| 309 | "query", | ||
| 310 | "public", | ||
| 311 | { | ||
| 312 | sessionId?: string; | ||
| 313 | status?: "pending" | "approved" | "denied"; | ||
| 314 | workspaceId: string; | ||
| 315 | }, | ||
| 316 | any | ||
| 317 | >; | ||
| 318 | listCredentialProviders: FunctionReference<"query", "public", {}, any>; | ||
| 319 | listCredentials: FunctionReference< | ||
| 320 | "query", | ||
| 321 | "public", | ||
| 322 | { sessionId?: string; workspaceId: string }, | ||
| 323 | any | ||
| 324 | >; | ||
| 325 | listPendingApprovals: FunctionReference< | ||
| 326 | "query", | ||
| 327 | "public", | ||
| 328 | { sessionId?: string; workspaceId: string }, | ||
| 329 | any | ||
| 330 | >; | ||
| 331 | listRuntimeTargets: FunctionReference<"query", "public", {}, any>; | ||
| 332 | listTaskEvents: FunctionReference< | ||
| 333 | "query", | ||
| 334 | "public", | ||
| 335 | { sessionId?: string; taskId: string; workspaceId: string }, | ||
| 336 | any | ||
| 337 | >; | ||
| 338 | listTasks: FunctionReference< | ||
| 339 | "query", | ||
| 340 | "public", | ||
| 341 | { sessionId?: string; workspaceId: string }, | ||
| 342 | any | ||
| 343 | >; | ||
| 344 | listToolSources: FunctionReference< | ||
| 345 | "query", | ||
| 346 | "public", | ||
| 347 | { sessionId?: string; workspaceId: string }, | ||
| 348 | any | ||
| 349 | >; | ||
| 350 | resolveCredential: FunctionReference< | ||
| 351 | "query", | ||
| 352 | "public", | ||
| 353 | { | ||
| 354 | actorId?: string; | ||
| 355 | scope: "workspace" | "actor"; | ||
| 356 | sessionId?: string; | ||
| 357 | sourceKey: string; | ||
| 358 | workspaceId: string; | ||
| 359 | }, | ||
| 360 | any | ||
| 361 | >; | ||
| 362 | upsertAccessPolicy: FunctionReference< | ||
| 363 | "mutation", | ||
| 364 | "public", | ||
| 365 | { | ||
| 366 | actorId?: string; | ||
| 367 | clientId?: string; | ||
| 368 | decision: "allow" | "require_approval" | "deny"; | ||
| 369 | id?: string; | ||
| 370 | priority?: number; | ||
| 371 | sessionId?: string; | ||
| 372 | toolPathPattern: string; | ||
| 373 | workspaceId: string; | ||
| 374 | }, | ||
| 375 | any | ||
| 376 | >; | ||
| 377 | upsertCredential: FunctionReference< | ||
| 378 | "mutation", | ||
| 379 | "public", | ||
| 380 | { | ||
| 381 | actorId?: string; | ||
| 382 | id?: string; | ||
| 383 | provider?: "managed" | "workos-vault"; | ||
| 384 | scope: "workspace" | "actor"; | ||
| 385 | secretJson: any; | ||
| 386 | sessionId?: string; | ||
| 387 | sourceKey: string; | ||
| 388 | workspaceId: string; | ||
| 389 | }, | ||
| 390 | any | ||
| 391 | >; | ||
| 392 | upsertToolSource: FunctionReference< | ||
| 393 | "mutation", | ||
| 394 | "public", | ||
| 395 | { | ||
| 396 | config: any; | ||
| 397 | enabled?: boolean; | ||
| 398 | id?: string; | ||
| 399 | name: string; | ||
| 400 | sessionId?: string; | ||
| 401 | type: "mcp" | "openapi" | "graphql"; | ||
| 402 | workspaceId: string; | ||
| 403 | }, | ||
| 404 | any | ||
| 405 | >; | ||
| 92 | }; | 406 | }; |
| 93 | workspaces: { | 407 | workspaces: { |
| 94 | create: FunctionReference<"mutation", "public", any, any>; | 408 | create: FunctionReference< |
| 95 | list: FunctionReference<"query", "public", any, any>; | 409 | "mutation", |
| 96 | generateWorkspaceIconUploadUrl: FunctionReference<"mutation", "public", any, any>; | 410 | "public", |
| 411 | { | ||
| 412 | iconStorageId?: Id<"_storage">; | ||
| 413 | name: string; | ||
| 414 | organizationId?: Id<"organizations">; | ||
| 415 | sessionId?: string; | ||
| 416 | }, | ||
| 417 | any | ||
| 418 | >; | ||
| 419 | generateWorkspaceIconUploadUrl: FunctionReference< | ||
| 420 | "mutation", | ||
| 421 | "public", | ||
| 422 | { sessionId?: string }, | ||
| 423 | any | ||
| 424 | >; | ||
| 425 | list: FunctionReference< | ||
| 426 | "query", | ||
| 427 | "public", | ||
| 428 | { organizationId?: Id<"organizations">; sessionId?: string }, | ||
| 429 | any | ||
| 430 | >; | ||
| 97 | }; | 431 | }; |
| 98 | }; | 432 | }; |
| 99 | 433 | ||
| @@ -107,79 +441,429 @@ export declare const api: { · 7 unmodified lines | |||
| 107 | */ | 441 | */ |
| 108 | export declare const internal: { | 442 | export declare const internal: { |
| 109 | auth: { | 443 | auth: { |
| 110 | authKitEvent: FunctionReference<"mutation", "internal", any, any>; | 444 | authKitEvent: FunctionReference< |
| 445 | "mutation", | ||
| 446 | "internal", | ||
| 447 | { data: Record<string, any>; event: string }, | ||
| 448 | null | ||
| 449 | >; | ||
| 111 | }; | 450 | }; |
| 112 | billingInternal: { | 451 | billingInternal: { |
| 113 | getBillingAccessForRequest: FunctionReference<"query", "internal", any, any>; | 452 | bumpSeatSyncVersion: FunctionReference< |
| 114 | getSeatSyncSnapshot: FunctionReference<"query", "internal", any, any>; | 453 | "mutation", |
| 115 | upsertCustomerLink: FunctionReference<"mutation", "internal", any, any>; | 454 | "internal", |
| 116 | bumpSeatSyncVersion: FunctionReference<"mutation", "internal", any, any>; | 455 | { organizationId: Id<"organizations"> }, |
| 117 | upsertSeatState: FunctionReference<"mutation", "internal", any, any>; | 456 | any |
| 457 | >; | ||
| 458 | getBillingAccessForRequest: FunctionReference< | ||
| 459 | "query", | ||
| 460 | "internal", | ||
| 461 | { organizationId: Id<"organizations">; sessionId?: string }, | ||
| 462 | any | ||
| 463 | >; | ||
| 464 | getSeatSyncSnapshot: FunctionReference< | ||
| 465 | "query", | ||
| 466 | "internal", | ||
| 467 | { organizationId: Id<"organizations"> }, | ||
| 468 | any | ||
| 469 | >; | ||
| 470 | upsertCustomerLink: FunctionReference< | ||
| 471 | "mutation", | ||
| 472 | "internal", | ||
| 473 | { organizationId: Id<"organizations">; stripeCustomerId: string }, | ||
| 474 | any | ||
| 475 | >; | ||
| 476 | upsertSeatState: FunctionReference< | ||
| 477 | "mutation", | ||
| 478 | "internal", | ||
| 479 | { | ||
| 480 | bumpVersion: boolean; | ||
| 481 | desiredSeats: number; | ||
| 482 | lastAppliedSeats: number | null; | ||
| 483 | organizationId: Id<"organizations">; | ||
| 484 | syncError: string | null; | ||
| 485 | }, | ||
| 486 | any | ||
| 487 | >; | ||
| 118 | }; | 488 | }; |
| 119 | billingSync: { | 489 | billingSync: { |
| 120 | syncSeatQuantity: FunctionReference<"action", "internal", any, any>; | 490 | syncSeatQuantity: FunctionReference< |
| 491 | "action", | ||
| 492 | "internal", | ||
| 493 | { expectedVersion: number; organizationId: Id<"organizations"> }, | ||
| 494 | any | ||
| 495 | >; | ||
| 121 | }; | 496 | }; |
| 122 | database: { | 497 | database: { |
| 123 | createTask: FunctionReference<"mutation", "internal", any, any>; | 498 | bootstrapAnonymousSession: FunctionReference< |
| 124 | getTask: FunctionReference<"query", "internal", any, any>; | 499 | "mutation", |
| 125 | listTasks: FunctionReference<"query", "internal", any, any>; | 500 | "internal", |
| 126 | listQueuedTaskIds: FunctionReference<"query", "internal", any, any>; | 501 | { sessionId?: string }, |
| 127 | listRuntimeTargets: FunctionReference<"query", "internal", any, any>; | 502 | any |
| 128 | getTaskInWorkspace: FunctionReference<"query", "internal", any, any>; | 503 | >; |
| 129 | markTaskRunning: FunctionReference<"mutation", "internal", any, any>; | 504 | createApproval: FunctionReference< |
| 130 | markTaskFinished: FunctionReference<"mutation", "internal", any, any>; | 505 | "mutation", |
| 131 | createApproval: FunctionReference<"mutation", "internal", any, any>; | 506 | "internal", |
| 132 | getApproval: FunctionReference<"query", "internal", any, any>; | 507 | { id: string; input?: any; taskId: string; toolPath: string }, |
| 133 | listApprovals: FunctionReference<"query", "internal", any, any>; | 508 | any |
| 134 | listPendingApprovals: FunctionReference<"query", "internal", any, any>; | 509 | >; |
| 135 | resolveApproval: FunctionReference<"mutation", "internal", any, any>; | 510 | createTask: FunctionReference< |
| 136 | getApprovalInWorkspace: FunctionReference<"query", "internal", any, any>; | 511 | "mutation", |
| 137 | bootstrapAnonymousSession: FunctionReference<"mutation", "internal", any, any>; | 512 | "internal", |
| 138 | upsertAccessPolicy: FunctionReference<"mutation", "internal", any, any>; | 513 | { |
| 139 | listAccessPolicies: FunctionReference<"query", "internal", any, any>; | 514 | actorId: string; |
| 140 | upsertCredential: FunctionReference<"mutation", "internal", any, any>; | 515 | clientId?: string; |
| 141 | listCredentials: FunctionReference<"query", "internal", any, any>; | 516 | code: string; |
| 142 | listCredentialProviders: FunctionReference<"query", "internal", any, any>; | 517 | id: string; |
| 143 | resolveCredential: FunctionReference<"query", "internal", any, any>; | 518 | metadata?: any; |
| 144 | upsertToolSource: FunctionReference<"mutation", "internal", any, any>; | 519 | runtimeId: string; |
| 145 | listToolSources: FunctionReference<"query", "internal", any, any>; | 520 | timeoutMs?: number; |
| 146 | deleteToolSource: FunctionReference<"mutation", "internal", any, any>; | 521 | workspaceId: string; |
| 147 | createTaskEvent: FunctionReference<"mutation", "internal", any, any>; | 522 | }, |
| 148 | listTaskEvents: FunctionReference<"query", "internal", any, any>; | 523 | any |
| 524 | >; | ||
| 525 | createTaskEvent: FunctionReference< | ||
| 526 | "mutation", | ||
| 527 | "internal", | ||
| 528 | { eventName: string; payload: any; taskId: string; type: string }, | ||
| 529 | any | ||
| 530 | >; | ||
| 531 | deleteToolSource: FunctionReference< | ||
| 532 | "mutation", | ||
| 533 | "internal", | ||
| 534 | { sourceId: string; workspaceId: string }, | ||
| 535 | any | ||
| 536 | >; | ||
| 537 | getApproval: FunctionReference< | ||
| 538 | "query", | ||
| 539 | "internal", | ||
| 540 | { approvalId: string }, | ||
| 541 | any | ||
| 542 | >; | ||
| 543 | getApprovalInWorkspace: FunctionReference< | ||
| 544 | "query", | ||
| 545 | "internal", | ||
| 546 | { approvalId: string; workspaceId: string }, | ||
| 547 | any | ||
| 548 | >; | ||
| 549 | getTask: FunctionReference<"query", "internal", { taskId: string }, any>; | ||
| 550 | getTaskInWorkspace: FunctionReference< | ||
| 551 | "query", | ||
| 552 | "internal", | ||
| 553 | { taskId: string; workspaceId: string }, | ||
| 554 | any | ||
| 555 | >; | ||
| 556 | listAccessPolicies: FunctionReference< | ||
| 557 | "query", | ||
| 558 | "internal", | ||
| 559 | { workspaceId: string }, | ||
| 560 | any | ||
| 561 | >; | ||
| 562 | listApprovals: FunctionReference< | ||
| 563 | "query", | ||
| 564 | "internal", | ||
| 565 | { status?: "pending" | "approved" | "denied"; workspaceId: string }, | ||
| 566 | any | ||
| 567 | >; | ||
| 568 | listCredentialProviders: FunctionReference<"query", "internal", {}, any>; | ||
| 569 | listCredentials: FunctionReference< | ||
| 570 | "query", | ||
| 571 | "internal", | ||
| 572 | { workspaceId: string }, | ||
| 573 | any | ||
| 574 | >; | ||
| 575 | listPendingApprovals: FunctionReference< | ||
| 576 | "query", | ||
| 577 | "internal", | ||
| 578 | { workspaceId: string }, | ||
| 579 | any | ||
| 580 | >; | ||
| 581 | listQueuedTaskIds: FunctionReference< | ||
| 582 | "query", | ||
| 583 | "internal", | ||
| 584 | { limit?: number }, | ||
| 585 | any | ||
| 586 | >; | ||
| 587 | listRuntimeTargets: FunctionReference<"query", "internal", {}, any>; | ||
| 588 | listTaskEvents: FunctionReference< | ||
| 589 | "query", | ||
| 590 | "internal", | ||
| 591 | { taskId: string }, | ||
| 592 | any | ||
| 593 | >; | ||
| 594 | listTasks: FunctionReference< | ||
| 595 | "query", | ||
| 596 | "internal", | ||
| 597 | { workspaceId: string }, | ||
| 598 | any | ||
| 599 | >; | ||
| 600 | listToolSources: FunctionReference< | ||
| 601 | "query", | ||
| 602 | "internal", | ||
| 603 | { workspaceId: string }, | ||
| 604 | any | ||
| 605 | >; | ||
| 606 | markTaskFinished: FunctionReference< | ||
| 607 | "mutation", | ||
| 608 | "internal", | ||
| 609 | { | ||
| 610 | error?: string; | ||
| 611 | exitCode?: number; | ||
| 612 | status: "completed" | "failed" | "timed_out" | "denied"; | ||
| 613 | stderr: string; | ||
| 614 | stdout: string; | ||
| 615 | taskId: string; | ||
| 616 | }, | ||
| 617 | any | ||
| 618 | >; | ||
| 619 | markTaskRunning: FunctionReference< | ||
| 620 | "mutation", | ||
| 621 | "internal", | ||
| 622 | { taskId: string }, | ||
| 623 | any | ||
| 624 | >; | ||
| 625 | resolveApproval: FunctionReference< | ||
| 626 | "mutation", | ||
| 627 | "internal", | ||
| 628 | { | ||
| 629 | approvalId: string; | ||
| 630 | decision: "approved" | "denied"; | ||
| 631 | reason?: string; | ||
| 632 | reviewerId?: string; | ||
| 633 | }, | ||
| 634 | any | ||
| 635 | >; | ||
| 636 | resolveCredential: FunctionReference< | ||
| 637 | "query", | ||
| 638 | "internal", | ||
| 639 | { | ||
| 640 | actorId?: string; | ||
| 641 | scope: "workspace" | "actor"; | ||
| 642 | sourceKey: string; | ||
| 643 | workspaceId: string; | ||
| 644 | }, | ||
| 645 | any | ||
| 646 | >; | ||
| 647 | upsertAccessPolicy: FunctionReference< | ||
| 648 | "mutation", | ||
| 649 | "internal", | ||
| 650 | { | ||
| 651 | actorId?: string; | ||
| 652 | clientId?: string; | ||
| 653 | decision: "allow" | "require_approval" | "deny"; | ||
| 654 | id?: string; | ||
| 655 | priority?: number; | ||
| 656 | toolPathPattern: string; | ||
| 657 | workspaceId: string; | ||
| 658 | }, | ||
| 659 | any | ||
| 660 | >; | ||
| 661 | upsertCredential: FunctionReference< | ||
| 662 | "mutation", | ||
| 663 | "internal", | ||
| 664 | { | ||
| 665 | actorId?: string; | ||
| 666 | id?: string; | ||
| 667 | provider?: "managed" | "workos-vault"; | ||
| 668 | scope: "workspace" | "actor"; | ||
| 669 | secretJson: any; | ||
| 670 | sourceKey: string; | ||
| 671 | workspaceId: string; | ||
| 672 | }, | ||
| 673 | any | ||
| 674 | >; | ||
| 675 | upsertToolSource: FunctionReference< | ||
| 676 | "mutation", | ||
| 677 | "internal", | ||
| 678 | { | ||
| 679 | config: any; | ||
| 680 | enabled?: boolean; | ||
| 681 | id?: string; | ||
| 682 | name: string; | ||
| 683 | type: "mcp" | "openapi" | "graphql"; | ||
| 684 | workspaceId: string; | ||
| 685 | }, | ||
| 686 | any | ||
| 687 | >; | ||
| 149 | }; | 688 | }; |
| 150 | executor: { | 689 | executor: { |
| 151 | createTaskInternal: FunctionReference<"mutation", "internal", any, any>; | 690 | appendRuntimeOutput: FunctionReference< |
| 152 | resolveApprovalInternal: FunctionReference<"mutation", "internal", any, any>; | 691 | "mutation", |
| 153 | appendRuntimeOutput: FunctionReference<"mutation", "internal", any, any>; | 692 | "internal", |
| 693 | { | ||
| 694 | line: string; | ||
| 695 | runId: string; | ||
| 696 | stream: "stdout" | "stderr"; | ||
| 697 | timestamp?: number; | ||
| 698 | }, | ||
| 699 | any | ||
| 700 | >; | ||
| 701 | createTaskInternal: FunctionReference< | ||
| 702 | "mutation", | ||
| 703 | "internal", | ||
| 704 | { | ||
| 705 | actorId: string; | ||
| 706 | clientId?: string; | ||
| 707 | code: string; | ||
| 708 | metadata?: any; | ||
| 709 | runtimeId?: string; | ||
| 710 | timeoutMs?: number; | ||
| 711 | workspaceId: string; | ||
| 712 | }, | ||
| 713 | any | ||
| 714 | >; | ||
| 715 | resolveApprovalInternal: FunctionReference< | ||
| 716 | "mutation", | ||
| 717 | "internal", | ||
| 718 | { | ||
| 719 | approvalId: string; | ||
| 720 | decision: "approved" | "denied"; | ||
| 721 | reason?: string; | ||
| 722 | reviewerId?: string; | ||
| 723 | workspaceId: string; | ||
| 724 | }, | ||
| 725 | any | ||
| 726 | >; | ||
| 154 | }; | 727 | }; |
| 155 | executorNode: { | 728 | executorNode: { |
| 156 | listToolsInternal: FunctionReference<"action", "internal", any, any>; | 729 | handleExternalToolCall: FunctionReference< |
| 157 | listToolsWithWarningsInternal: FunctionReference<"action", "internal", any, any>; | 730 | "action", |
| 158 | handleExternalToolCall: FunctionReference<"action", "internal", any, any>; | 731 | "internal", |
| 159 | runTask: FunctionReference<"action", "internal", any, any>; | 732 | { callId: string; input?: any; runId: string; toolPath: string }, |
| 733 | any | ||
| 734 | >; | ||
| 735 | listToolsInternal: FunctionReference< | ||
| 736 | "action", | ||
| 737 | "internal", | ||
| 738 | { actorId?: string; clientId?: string; workspaceId: string }, | ||
| 739 | any | ||
| 740 | >; | ||
| 741 | listToolsWithWarningsInternal: FunctionReference< | ||
| 742 | "action", | ||
| 743 | "internal", | ||
| 744 | { actorId?: string; clientId?: string; workspaceId: string }, | ||
| 745 | any | ||
| 746 | >; | ||
| 747 | runTask: FunctionReference<"action", "internal", { taskId: string }, any>; | ||
| 160 | }; | 748 | }; |
| 161 | invites: { | 749 | invites: { |
| 162 | deliverWorkosInvite: FunctionReference<"action", "internal", any, any>; | 750 | deliverWorkosInvite: FunctionReference< |
| 163 | revokeWorkosInvite: FunctionReference<"action", "internal", any, any>; | 751 | "action", |
| 164 | getInviteDeliveryContext: FunctionReference<"query", "internal", any, any>; | 752 | "internal", |
| 165 | linkOrganizationToWorkos: FunctionReference<"mutation", "internal", any, any>; | 753 | { |
| 166 | getInviteById: FunctionReference<"query", "internal", any, any>; | 754 | expiresInDays?: number; |
| 167 | markInviteDelivered: FunctionReference<"mutation", "internal", any, any>; | 755 | inviteId: Id<"invites">; |
| 168 | markInviteDeliveryFailed: FunctionReference<"mutation", "internal", any, any>; | 756 | inviterWorkosUserId: string; |
| 757 | roleSlug?: string; | ||
| 758 | }, | ||
| 759 | any | ||
| 760 | >; | ||
| 761 | getInviteById: FunctionReference< | ||
| 762 | "query", | ||
| 763 | "internal", | ||
| 764 | { inviteId: Id<"invites"> }, | ||
| 765 | any | ||
| 766 | >; | ||
| 767 | getInviteDeliveryContext: FunctionReference< | ||
| 768 | "query", | ||
| 769 | "internal", | ||
| 770 | { inviteId: Id<"invites"> }, | ||
| 771 | any | ||
| 772 | >; | ||
| 773 | linkOrganizationToWorkos: FunctionReference< | ||
| 774 | "mutation", | ||
| 775 | "internal", | ||
| 776 | { | ||
| 777 | organizationId: Id<"organizations">; | ||
| 778 | workosOrgId: string; | ||
| 779 | workspaceId?: Id<"workspaces">; | ||
| 780 | }, | ||
| 781 | any | ||
| 782 | >; | ||
| 783 | markInviteDelivered: FunctionReference< | ||
| 784 | "mutation", | ||
| 785 | "internal", | ||
| 786 | { inviteId: Id<"invites">; providerInviteId: string }, | ||
| 787 | any | ||
| 788 | >; | ||
| 789 | markInviteDeliveryFailed: FunctionReference< | ||
| 790 | "mutation", | ||
| 791 | "internal", | ||
| 792 | { errorMessage: string; inviteId: Id<"invites"> }, | ||
| 793 | any | ||
| 794 | >; | ||
| 795 | revokeWorkosInvite: FunctionReference< | ||
| 796 | "action", | ||
| 797 | "internal", | ||
| 798 | { inviteId: Id<"invites">; providerInviteId: string }, | ||
| 799 | any | ||
| 800 | >; | ||
| 169 | }; | 801 | }; |
| 170 | openApiSpecCache: { | 802 | openApiSpecCache: { |
| 171 | getEntry: FunctionReference<"query", "internal", any, any>; | 803 | getEntry: FunctionReference< |
| 172 | putEntry: FunctionReference<"mutation", "internal", any, any>; | 804 | "query", |
| 173 | pruneExpired: FunctionReference<"mutation", "internal", any, any>; | 805 | "internal", |
| 806 | { maxAgeMs: number; specUrl: string; version: string }, | ||
| 807 | any | ||
| 808 | >; | ||
| 809 | pruneExpired: FunctionReference< | ||
| 810 | "mutation", | ||
| 811 | "internal", | ||
| 812 | { batchSize?: number; maxAgeMs: number }, | ||
| 813 | any | ||
| 814 | >; | ||
| 815 | putEntry: FunctionReference< | ||
| 816 | "mutation", | ||
| 817 | "internal", | ||
| 818 | { | ||
| 819 | sizeBytes: number; | ||
| 820 | specUrl: string; | ||
| 821 | storageId: Id<"_storage">; | ||
| 822 | version: string; | ||
| 823 | }, | ||
| 824 | any | ||
| 825 | >; | ||
| 174 | }; | 826 | }; |
| 175 | workspaceAuthInternal: { | 827 | workspaceAuthInternal: { |
| 176 | getWorkspaceAccessForRequest: FunctionReference<"query", "internal", any, any>; | 828 | getWorkspaceAccessForRequest: FunctionReference< |
| 177 | getWorkspaceAccessForWorkosSubject: FunctionReference<"query", "internal", any, any>; | 829 | "query", |
| 830 | "internal", | ||
| 831 | { sessionId?: string; workspaceId: string }, | ||
| 832 | any | ||
| 833 | >; | ||
| 834 | getWorkspaceAccessForWorkosSubject: FunctionReference< | ||
| 835 | "query", | ||
| 836 | "internal", | ||
| 837 | { subject: string; workspaceId: string }, | ||
| 838 | any | ||
| 839 | >; | ||
| 178 | }; | 840 | }; |
| 179 | workspaceToolCache: { | 841 | workspaceToolCache: { |
| 180 | getEntry: FunctionReference<"query", "internal", any, any>; | 842 | getDtsStorageIds: FunctionReference< |
| 181 | putEntry: FunctionReference<"mutation", "internal", any, any>; | 843 | "query", |
| 182 | getDtsStorageIds: FunctionReference<"query", "internal", any, any>; | 844 | "internal", |
| 845 | { workspaceId: string }, | ||
| 846 | any | ||
| 847 | >; | ||
| 848 | getEntry: FunctionReference< | ||
| 849 | "query", | ||
| 850 | "internal", | ||
| 851 | { signature: string; workspaceId: string }, | ||
| 852 | any | ||
| 853 | >; | ||
| 854 | putEntry: FunctionReference< | ||
| 855 | "mutation", | ||
| 856 | "internal", | ||
| 857 | { | ||
| 858 | dtsStorageIds?: Array<{ sourceKey: string; storageId: Id<"_storage"> }>; | ||
| 859 | signature: string; | ||
| 860 | sizeBytes: number; | ||
| 861 | storageId: Id<"_storage">; | ||
| 862 | toolCount: number; | ||
| 863 | workspaceId: string; | ||
| 864 | }, | ||
| 865 | any | ||
| 866 | >; | ||
| 183 | }; | 867 | }; |
| 184 | }; | 868 | }; |
| 185 | 869 | ||
| @@ -573,4 +1257,3 @@ export declare const components: { · 387 unmodified lines | |||
| 573 | }; | 1257 | }; |
| 574 | }; | 1258 | }; |
| 575 | }; | 1259 | }; |
| 576 | |||
| @@ -115,8 +115,14 @@ export const createSubscriptionCheckout = action({ · 114 unmodified lines | |||
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | const quantity = Math.max(1, access.billableMembers); | 117 | const quantity = Math.max(1, access.billableMembers); |
| 118 | const successUrl = args.successUrl ?? process.env.BILLING_SUCCESS_URL ?? "http://localhost:3000/billing?success=true"; | 118 | const successUrl = |
| 119 | const cancelUrl = args.cancelUrl ?? process.env.BILLING_CANCEL_URL ?? "http://localhost:3000/billing?canceled=true"; | 119 | args.successUrl |
| 120 | ?? process.env.BILLING_SUCCESS_URL | ||
| 121 | ?? "http://localhost:3000/organization?tab=billing&success=true"; | ||
| 122 | const cancelUrl = | ||
| 123 | args.cancelUrl | ||
| 124 | ?? process.env.BILLING_CANCEL_URL | ||
| 125 | ?? "http://localhost:3000/organization?tab=billing&canceled=true"; | ||
| 120 | 126 | ||
| 121 | const session = await stripeClient.createCheckoutSession(ctx, { | 127 | const session = await stripeClient.createCheckoutSession(ctx, { |
| 122 | priceId: args.priceId, | 128 | priceId: args.priceId, |
| @@ -187,7 +193,7 @@ export const createCustomerPortal = action({ · 64 unmodified lines | |||
| 187 | 193 | ||
| 188 | return await stripeClient.createCustomerPortalSession(ctx, { | 194 | return await stripeClient.createCustomerPortalSession(ctx, { |
| 189 | customerId, | 195 | customerId, |
| 190 | returnUrl: args.returnUrl ?? process.env.BILLING_RETURN_URL ?? "http://localhost:3000/billing", | 196 | returnUrl: args.returnUrl ?? process.env.BILLING_RETURN_URL ?? "http://localhost:3000/organization?tab=billing", |
| 191 | }); | 197 | }); |
| 192 | }, | 198 | }, |
| 193 | }); | 199 | }); |
| @@ -56,7 +56,7 @@ function policySpecificity(policy: AccessPolicyRecord, actorId?: string, clientI · 55 unmodified lines | |||
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string { | 58 | function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string { |
| 59 | const signatureVersion = "v2"; | 59 | const signatureVersion = "v4"; |
| 60 | const parts = sources | 60 | const parts = sources |
| 61 | .map((source) => `${source.id}:${source.updatedAt}:${source.enabled ? 1 : 0}`) | 61 | .map((source) => `${source.id}:${source.updatedAt}:${source.enabled ? 1 : 0}`) |
| 62 | .sort(); | 62 | .sort(); |
| @@ -131,7 +131,7 @@ interface DtsStorageEntry { · 68 unmodified lines | |||
| 131 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; | 131 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; |
| 132 | 132 | ||
| 133 | /** Cache version — bump when PreparedOpenApiSpec shape changes. */ | 133 | /** Cache version — bump when PreparedOpenApiSpec shape changes. */ |
| 134 | const OPENAPI_CACHE_VERSION = "v4"; | 134 | const OPENAPI_CACHE_VERSION = "v7"; |
| 135 | 135 | ||
| 136 | async function publish( | 136 | async function publish( |
| 137 | ctx: any, | 137 | ctx: any, |
| @@ -314,6 +314,114 @@ describe("buildOpenApiToolsFromPrepared", () => { · 313 unmodified lines | |||
| 314 | } | 314 | } |
| 315 | }); | 315 | }); |
| 316 | 316 | ||
| 317 | test("resolves shared parameter refs and maps 204 responses to void", async () => { | ||
| 318 | const spec: Record<string, unknown> = { | ||
| 319 | openapi: "3.0.3", | ||
| 320 | info: { title: "GitHub-like", version: "1.0.0" }, | ||
| 321 | servers: [{ url: "https://api.example.com" }], | ||
| 322 | components: { | ||
| 323 | parameters: { | ||
| 324 | owner: { | ||
| 325 | name: "owner", | ||
| 326 | in: "path", | ||
| 327 | required: true, | ||
| 328 | schema: { type: "string" }, | ||
| 329 | }, | ||
| 330 | repo: { | ||
| 331 | name: "repo", | ||
| 332 | in: "path", | ||
| 333 | required: true, | ||
| 334 | schema: { type: "string" }, | ||
| 335 | }, | ||
| 336 | }, | ||
| 337 | }, | ||
| 338 | paths: { | ||
| 339 | "/repos/{owner}/{repo}/subscription": { | ||
| 340 | parameters: [ | ||
| 341 | { $ref: "#/components/parameters/owner" }, | ||
| 342 | { $ref: "#/components/parameters/repo" }, | ||
| 343 | ], | ||
| 344 | delete: { | ||
| 345 | operationId: "activity/delete-repo-subscription", | ||
| 346 | tags: ["activity"], | ||
| 347 | responses: { | ||
| 348 | "204": { description: "No Content" }, | ||
| 349 | }, | ||
| 350 | }, | ||
| 351 | }, | ||
| 352 | }, | ||
| 353 | }; | ||
| 354 | |||
| 355 | const prepared = await prepareOpenApiSpec(spec, "github-like"); | ||
| 356 | const tools = buildOpenApiToolsFromPrepared( | ||
| 357 | { | ||
| 358 | type: "openapi", | ||
| 359 | name: "github", | ||
| 360 | spec, | ||
| 361 | baseUrl: "https://api.example.com", | ||
| 362 | }, | ||
| 363 | prepared, | ||
| 364 | ); | ||
| 365 | |||
| 366 | const tool = tools.find((t) => t.metadata?.operationId === "activity/delete-repo-subscription"); | ||
| 367 | expect(tool).toBeDefined(); | ||
| 368 | expect(tool!.metadata?.argsType).toContain("owner"); | ||
| 369 | expect(tool!.metadata?.argsType).toContain("repo"); | ||
| 370 | expect(tool!.metadata?.returnsType).toBe("void"); | ||
| 371 | }); | ||
| 372 | |||
| 373 | test("supports Swagger 2.x parameter and response schemas for type hints", async () => { | ||
| 374 | const spec: Record<string, unknown> = { | ||
| 375 | swagger: "2.0", | ||
| 376 | info: { title: "Slack-like", version: "1.0.0" }, | ||
| 377 | host: "api.example.com", | ||
| 378 | schemes: ["https"], | ||
| 379 | basePath: "/api", | ||
| 380 | paths: { | ||
| 381 | "/admin.apps.approved.list": { | ||
| 382 | get: { | ||
| 383 | operationId: "admin_apps_approved_list", | ||
| 384 | tags: ["admin.apps.approved"], | ||
| 385 | parameters: [ | ||
| 386 | { name: "token", in: "query", required: true, type: "string" }, | ||
| 387 | { name: "limit", in: "query", required: false, type: "integer" }, | ||
| 388 | ], | ||
| 389 | responses: { | ||
| 390 | "200": { | ||
| 391 | description: "ok", | ||
| 392 | schema: { | ||
| 393 | type: "object", | ||
| 394 | properties: { | ||
| 395 | ok: { type: "boolean" }, | ||
| 396 | }, | ||
| 397 | required: ["ok"], | ||
| 398 | }, | ||
| 399 | }, | ||
| 400 | }, | ||
| 401 | }, | ||
| 402 | }, | ||
| 403 | }, | ||
| 404 | }; | ||
| 405 | |||
| 406 | const prepared = await prepareOpenApiSpec(spec, "slack-like"); | ||
| 407 | const tools = buildOpenApiToolsFromPrepared( | ||
| 408 | { | ||
| 409 | type: "openapi", | ||
| 410 | name: "slack", | ||
| 411 | spec, | ||
| 412 | baseUrl: "https://api.example.com/api", | ||
| 413 | }, | ||
| 414 | prepared, | ||
| 415 | ); | ||
| 416 | |||
| 417 | const tool = tools.find((t) => t.metadata?.operationId === "admin_apps_approved_list"); | ||
| 418 | expect(tool).toBeDefined(); | ||
| 419 | expect(tool!.metadata?.argsType).toContain("token: string"); | ||
| 420 | expect(tool!.metadata?.argsType).toContain("limit?: number"); | ||
| 421 | expect(tool!.metadata?.returnsType).toContain("ok"); | ||
| 422 | expect(tool!.metadata?.returnsType).not.toBe("unknown"); | ||
| 423 | }); | ||
| 424 | |||
| 317 | test("schemaTypes only attached to first tool from a source", async () => { | 425 | test("schemaTypes only attached to first tool from a source", async () => { |
| 318 | const specWithSchemas: Record<string, unknown> = { | 426 | const specWithSchemas: Record<string, unknown> = { |
| 319 | openapi: "3.0.3", | 427 | openapi: "3.0.3", |
| @@ -154,6 +154,16 @@ describe("real-world OpenAPI specs", () => { · 153 unmodified lines | |||
| 154 | // The sourceDts should contain operations interface | 154 | // The sourceDts should contain operations interface |
| 155 | const dts = withSourceDts[0].metadata!.sourceDts!; | 155 | const dts = withSourceDts[0].metadata!.sourceDts!; |
| 156 | expect(dts).toContain("operations"); | 156 | expect(dts).toContain("operations"); |
| 157 | |||
| 158 | // Real specs should produce useful type hints for at least some operations. | ||
| 159 | const typedInputs = tools.filter( | ||
| 160 | (t) => t.metadata?.argsType && t.metadata.argsType !== "Record<string, unknown>", | ||
| 161 | ); | ||
| 162 | const typedOutputs = tools.filter( | ||
| 163 | (t) => t.metadata?.returnsType && t.metadata.returnsType !== "unknown", | ||
| 164 | ); | ||
| 165 | expect(typedInputs.length).toBeGreaterThan(0); | ||
| 166 | expect(typedOutputs.length).toBeGreaterThan(0); | ||
| 157 | } | 167 | } |
| 158 | 168 | ||
| 159 | if (prepared.warnings.length > 0) { | 169 | if (prepared.warnings.length > 0) { |
| @@ -164,4 +174,64 @@ describe("real-world OpenAPI specs", () => { · 4 unmodified lines | |||
| 164 | 300_000, | 174 | 300_000, |
| 165 | ); | 175 | ); |
| 166 | } | 176 | } |
| 177 | |||
| 178 | test( | ||
| 179 | "github: delete repo subscription keeps typed args and void return", | ||
| 180 | async () => { | ||
| 181 | const githubUrl = | ||
| 182 | "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml"; | ||
| 183 | |||
| 184 | const prepared = await prepareOpenApiSpec(githubUrl, "github"); | ||
| 185 | const tools = buildOpenApiToolsFromPrepared( | ||
| 186 | { | ||
| 187 | type: "openapi", | ||
| 188 | name: "github", | ||
| 189 | spec: githubUrl, | ||
| 190 | baseUrl: prepared.servers[0] || "https://api.github.com", | ||
| 191 | }, | ||
| 192 | prepared, | ||
| 193 | ); | ||
| 194 | |||
| 195 | const tool = tools.find( | ||
| 196 | (t) => t.metadata?.operationId === "activity/delete-repo-subscription", | ||
| 197 | ); | ||
| 198 | |||
| 199 | expect(tool).toBeDefined(); | ||
| 200 | expect(tool!.path).toBe("github.activity.activity_delete_repo_subscription"); | ||
| 201 | expect(tool!.metadata!.argsType).toContain("owner"); | ||
| 202 | expect(tool!.metadata!.argsType).toContain("repo"); | ||
| 203 | expect(tool!.metadata!.argsType).not.toBe("Record<string, unknown>"); | ||
| 204 | expect(tool!.metadata!.returnsType).toBe("void"); | ||
| 205 | }, | ||
| 206 | 300_000, | ||
| 207 | ); | ||
| 208 | |||
| 209 | test( | ||
| 210 | "slack: approved apps list keeps typed query params and non-unknown output", | ||
| 211 | async () => { | ||
| 212 | const slackUrl = "https://api.slack.com/specs/openapi/v2/slack_web.json"; | ||
| 213 | |||
| 214 | const prepared = await prepareOpenApiSpec(slackUrl, "slack"); | ||
| 215 | const tools = buildOpenApiToolsFromPrepared( | ||
| 216 | { | ||
| 217 | type: "openapi", | ||
| 218 | name: "slack", | ||
| 219 | spec: slackUrl, | ||
| 220 | baseUrl: "https://slack.com/api", | ||
| 221 | }, | ||
| 222 | prepared, | ||
| 223 | ); | ||
| 224 | |||
| 225 | const tool = tools.find( | ||
| 226 | (t) => t.metadata?.operationId === "admin_apps_approved_list", | ||
| 227 | ); | ||
| 228 | |||
| 229 | expect(tool).toBeDefined(); | ||
| 230 | expect(tool!.path).toBe("slack.admin_apps_approved.admin_apps_approved_list"); | ||
| 231 | expect(tool!.metadata!.argsType).toContain("token: string"); | ||
| 232 | expect(tool!.metadata!.argsType).toContain("limit?: number"); | ||
| 233 | expect(tool!.metadata!.returnsType).not.toBe("unknown"); | ||
| 234 | }, | ||
| 235 | 300_000, | ||
| 236 | ); | ||
| 167 | }); | 237 | }); |
| @@ -232,6 +232,78 @@ function getPreferredContentSchema(content: Record<string, unknown>): Record<str · 231 unmodified lines | |||
| 232 | return {}; | 232 | return {}; |
| 233 | } | 233 | } |
| 234 | 234 | ||
| 235 | function getPreferredResponseSchema(responseValue: Record<string, unknown>): Record<string, unknown> { | ||
| 236 | // OpenAPI 3.x response shape | ||
| 237 | const contentSchema = getPreferredContentSchema(asRecord(responseValue.content)); | ||
| 238 | if (Object.keys(contentSchema).length > 0) { | ||
| 239 | return contentSchema; | ||
| 240 | } | ||
| 241 | |||
| 242 | // Swagger 2.x response shape | ||
| 243 | const schema = asRecord(responseValue.schema); | ||
| 244 | if (Object.keys(schema).length > 0) { | ||
| 245 | return schema; | ||
| 246 | } | ||
| 247 | |||
| 248 | return {}; | ||
| 249 | } | ||
| 250 | |||
| 251 | function resolveSchemaRef( | ||
| 252 | schema: Record<string, unknown>, | ||
| 253 | componentSchemas: Record<string, unknown>, | ||
| 254 | ): Record<string, unknown> { | ||
| 255 | const ref = typeof schema.$ref === "string" ? schema.$ref : ""; | ||
| 256 | const prefix = "#/components/schemas/"; | ||
| 257 | if (!ref.startsWith(prefix)) { | ||
| 258 | return schema; | ||
| 259 | } | ||
| 260 | |||
| 261 | const key = ref.slice(prefix.length); | ||
| 262 | const resolved = asRecord(componentSchemas[key]); | ||
| 263 | if (Object.keys(resolved).length === 0) { | ||
| 264 | return schema; | ||
| 265 | } | ||
| 266 | return resolved; | ||
| 267 | } | ||
| 268 | |||
| 269 | function parameterSchemaFromEntry(entry: Record<string, unknown>): Record<string, unknown> { | ||
| 270 | // OpenAPI 3.x (and Swagger body params) use `schema` | ||
| 271 | const schema = asRecord(entry.schema); | ||
| 272 | if (Object.keys(schema).length > 0) { | ||
| 273 | return schema; | ||
| 274 | } | ||
| 275 | |||
| 276 | // Swagger 2.x non-body params use top-level `type`, `enum`, `items`, etc. | ||
| 277 | const type = typeof entry.type === "string" ? entry.type : ""; | ||
| 278 | if (!type) { | ||
| 279 | return {}; | ||
| 280 | } | ||
| 281 | |||
| 282 | const fallback: Record<string, unknown> = { type }; | ||
| 283 | if (Array.isArray(entry.enum) && entry.enum.length > 0) { | ||
| 284 | fallback.enum = entry.enum; | ||
| 285 | } | ||
| 286 | const items = asRecord(entry.items); | ||
| 287 | if (Object.keys(items).length > 0) { | ||
| 288 | fallback.items = items; | ||
| 289 | } | ||
| 290 | |||
| 291 | return fallback; | ||
| 292 | } | ||
| 293 | |||
| 294 | function responseTypeHintFromSchema(responseSchema: Record<string, unknown>, responseStatus: string): string { | ||
| 295 | if (Object.keys(responseSchema).length > 0) { | ||
| 296 | return jsonSchemaTypeHintFallback(responseSchema); | ||
| 297 | } | ||
| 298 | |||
| 299 | // No-content success responses should be represented as void. | ||
| 300 | if (responseStatus === "204" || responseStatus === "205") { | ||
| 301 | return "void"; | ||
| 302 | } | ||
| 303 | |||
| 304 | return "unknown"; | ||
| 305 | } | ||
| 306 | |||
| 235 | /** Simple depth-limited type hint generator for schemas (used as fallback) */ | 307 | /** Simple depth-limited type hint generator for schemas (used as fallback) */ |
| 236 | function jsonSchemaTypeHintFallback(schema: unknown, depth = 0): string { | 308 | function jsonSchemaTypeHintFallback(schema: unknown, depth = 0): string { |
| 237 | if (!schema || typeof schema !== "object") return "unknown"; | 309 | if (!schema || typeof schema !== "object") return "unknown"; |
| @@ -254,6 +326,7 @@ function jsonSchemaTypeHintFallback(schema: unknown, depth = 0): string { · 16 unmodified lines | |||
| 254 | } | 326 | } |
| 255 | 327 | ||
| 256 | const type = typeof shape.type === "string" ? shape.type : undefined; | 328 | const type = typeof shape.type === "string" ? shape.type : undefined; |
| 329 | if (type === "integer") return "number"; | ||
| 257 | if (type === "string" || type === "number" || type === "boolean" || type === "null") { | 330 | if (type === "string" || type === "number" || type === "boolean" || type === "null") { |
| 258 | return type; | 331 | return type; |
| 259 | } | 332 | } |
| @@ -466,21 +539,39 @@ function buildOpenApiUrl( · 206 unmodified lines | |||
| 466 | function compactOpenApiPaths( | 539 | function compactOpenApiPaths( |
| 467 | pathsValue: unknown, | 540 | pathsValue: unknown, |
| 468 | operationTypeIds: Set<string>, | 541 | operationTypeIds: Set<string>, |
| 542 | componentParameters?: Record<string, unknown>, | ||
| 543 | componentSchemas?: Record<string, unknown>, | ||
| 469 | ): Record<string, unknown> { | 544 | ): Record<string, unknown> { |
| 470 | const paths = asRecord(pathsValue); | 545 | const paths = asRecord(pathsValue); |
| 471 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; | 546 | const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const; |
| 472 | const compactPaths: Record<string, unknown> = {}; | 547 | const compactPaths: Record<string, unknown> = {}; |
| 548 | const compParams = componentParameters ? asRecord(componentParameters) : {}; | ||
| 549 | const compSchemas = componentSchemas ? asRecord(componentSchemas) : {}; | ||
| 550 | |||
| 551 | const resolveParam = (entry: Record<string, unknown>): Record<string, unknown> => { | ||
| 552 | if (typeof entry.$ref === "string") { | ||
| 553 | // Resolve "#/components/parameters/foo" → components.parameters.foo | ||
| 554 | const ref = entry.$ref; | ||
| 555 | const prefix = "#/components/parameters/"; | ||
| 556 | if (ref.startsWith(prefix)) { | ||
| 557 | const key = ref.slice(prefix.length); | ||
| 558 | const resolved = asRecord(compParams[key]); | ||
| 559 | if (Object.keys(resolved).length > 0) return resolved; | ||
| 560 | } | ||
| 561 | } | ||
| 562 | return entry; | ||
| 563 | }; | ||
| 473 | 564 | ||
| 474 | const normalizeParameters = (entries: unknown): Array<Record<string, unknown>> => { | 565 | const normalizeParameters = (entries: unknown): Array<Record<string, unknown>> => { |
| 475 | if (!Array.isArray(entries)) return []; | 566 | if (!Array.isArray(entries)) return []; |
| 476 | return entries | 567 | return entries |
| 477 | .map((entry) => asRecord(entry)) | 568 | .map((entry) => resolveParam(asRecord(entry))) |
| 478 | .filter((entry) => typeof entry.name === "string" && typeof entry.in === "string") | 569 | .filter((entry) => typeof entry.name === "string" && typeof entry.in === "string") |
| 479 | .map((entry) => ({ | 570 | .map((entry) => ({ |
| 480 | name: String(entry.name), | 571 | name: String(entry.name), |
| 481 | in: String(entry.in), | 572 | in: String(entry.in), |
| 482 | required: Boolean(entry.required), | 573 | required: Boolean(entry.required), |
| 483 | schema: asRecord(entry.schema), | 574 | schema: parameterSchemaFromEntry(entry), |
| 484 | })); | 575 | })); |
| 485 | }; | 576 | }; |
| 486 | 577 | ||
| @@ -523,15 +614,20 @@ function compactOpenApiPaths( · 36 unmodified lines | |||
| 523 | { | 614 | { |
| 524 | const requestBody = asRecord(operation.requestBody); | 615 | const requestBody = asRecord(operation.requestBody); |
| 525 | const requestBodyContent = asRecord(requestBody.content); | 616 | const requestBodyContent = asRecord(requestBody.content); |
| 526 | const requestBodySchema = getPreferredContentSchema(requestBodyContent); | 617 | const requestBodySchema = resolveSchemaRef( |
| 618 | getPreferredContentSchema(requestBodyContent), | ||
| 619 | compSchemas, | ||
| 620 | ); | ||
| 527 | 621 | ||
| 528 | const responses = asRecord(operation.responses); | 622 | const responses = asRecord(operation.responses); |
| 529 | let responseSchema: Record<string, unknown> = {}; | 623 | let responseSchema: Record<string, unknown> = {}; |
| 530 | let responseStatus = ""; | 624 | let responseStatus = ""; |
| 531 | for (const [status, responseValue] of Object.entries(responses)) { | 625 | for (const [status, responseValue] of Object.entries(responses)) { |
| 532 | if (!status.startsWith("2")) continue; | 626 | if (!status.startsWith("2")) continue; |
| 533 | const responseContent = asRecord(asRecord(responseValue).content); | 627 | responseSchema = resolveSchemaRef( |
| 534 | responseSchema = getPreferredContentSchema(responseContent); | 628 | getPreferredResponseSchema(asRecord(responseValue)), |
| 629 | compSchemas, | ||
| 630 | ); | ||
| 535 | responseStatus = status; | 631 | responseStatus = status; |
| 536 | if (Object.keys(responseSchema).length > 0) break; | 632 | if (Object.keys(responseSchema).length > 0) break; |
| 537 | } | 633 | } |
| @@ -559,7 +655,7 @@ function compactOpenApiPaths( · 21 unmodified lines | |||
| 559 | ], | 655 | ], |
| 560 | }; | 656 | }; |
| 561 | compactOperation._argsTypeHint = jsonSchemaTypeHintFallback(combinedSchema); | 657 | compactOperation._argsTypeHint = jsonSchemaTypeHintFallback(combinedSchema); |
| 562 | compactOperation._returnsTypeHint = jsonSchemaTypeHintFallback(responseSchema); | 658 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus); |
| 563 | } else { | 659 | } else { |
| 564 | // Keep full schemas for the fallback path | 660 | // Keep full schemas for the fallback path |
| 565 | if (Object.keys(requestBodySchema).length > 0) { | 661 | if (Object.keys(requestBodySchema).length > 0) { |
| @@ -665,7 +761,12 @@ export async function prepareOpenApiSpec( · 99 unmodified lines | |||
| 665 | servers: servers | 761 | servers: servers |
| 666 | .map((server) => (typeof server.url === "string" ? server.url : "")) | 762 | .map((server) => (typeof server.url === "string" ? server.url : "")) |
| 667 | .filter((url) => url.length > 0), | 763 | .filter((url) => url.length > 0), |
| 668 | paths: compactOpenApiPaths(bundled.paths, operationTypeIds), | 764 | paths: compactOpenApiPaths( |
| 765 | bundled.paths, | ||
| 766 | operationTypeIds, | ||
| 767 | asRecord(asRecord(bundled.components).parameters), | ||
| 768 | asRecord(asRecord(bundled.components).schemas), | ||
| 769 | ), | ||
| 669 | dts: dts ?? undefined, | 770 | dts: dts ?? undefined, |
| 670 | warnings, | 771 | warnings, |
| 671 | }; | 772 | }; |
| @@ -736,10 +837,11 @@ export function buildOpenApiToolsFromPrepared( · 64 unmodified lines | |||
| 736 | 837 | ||
| 737 | const responses = asRecord(operation.responses); | 838 | const responses = asRecord(operation.responses); |
| 738 | let responseSchema: Record<string, unknown> = {}; | 839 | let responseSchema: Record<string, unknown> = {}; |
| 840 | let responseStatus = ""; | ||
| 739 | for (const [status, responseValue] of Object.entries(responses)) { | 841 | for (const [status, responseValue] of Object.entries(responses)) { |
| 740 | if (!status.startsWith("2")) continue; | 842 | if (!status.startsWith("2")) continue; |
| 741 | const responseContent = asRecord(asRecord(responseValue).content); | 843 | responseSchema = getPreferredResponseSchema(asRecord(responseValue)); |
| 742 | responseSchema = getPreferredContentSchema(responseContent); | 844 | responseStatus = status; |
| 743 | if (Object.keys(responseSchema).length > 0) break; | 845 | if (Object.keys(responseSchema).length > 0) break; |
| 744 | } | 846 | } |
| 745 | 847 | ||
| @@ -758,7 +860,7 @@ export function buildOpenApiToolsFromPrepared( · 12 unmodified lines | |||
| 758 | }; | 860 | }; |
| 759 | 861 | ||
| 760 | argsType = jsonSchemaTypeHintFallback(combinedSchema); | 862 | argsType = jsonSchemaTypeHintFallback(combinedSchema); |
| 761 | returnsType = jsonSchemaTypeHintFallback(responseSchema); | 863 | returnsType = responseTypeHintFromSchema(responseSchema, responseStatus); |
| 762 | } | 864 | } |
| 763 | 865 | ||
| 764 | const approval = config.overrides?.[operationIdRaw]?.approval | 866 | const approval = config.overrides?.[operationIdRaw]?.approval |
| @@ -4,8 +4,7 @@ · 3 unmodified lines | |||
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "scripts": { | 5 | "scripts": { |
| 6 | "deploy": "bunx convex deploy", | 6 | "deploy": "bunx convex deploy", |
| 7 | "codegen": "bunx convex codegen && bun scripts/postcodegen.ts", | 7 | "codegen": "bunx convex codegen" |
| 8 | "fix-types": "bun scripts/postcodegen.ts" | ||
| 9 | }, | 8 | }, |
| 10 | "dependencies": { | 9 | "dependencies": { |
| 11 | "convex": "^1.31.7" | 10 | "convex": "^1.31.7" |
| @@ -1,289 +0,0 @@ | |||
| 1 | /** | ||
| 2 | * Post-codegen script for Convex. | ||
| 3 | * | ||
| 4 | * Rewrites `convex/_generated/api.d.ts` to replace the deeply-recursive | ||
| 5 | * `ApiFromModules` / `FilterApi` types with explicit per-module declarations. | ||
| 6 | * | ||
| 7 | * Why: With 18+ modules and 100+ exported functions, the recursive type chain | ||
| 8 | * `ApiFromModules → ExpandModulesAndDirs → UnionToIntersection → FilterApi` | ||
| 9 | * exceeds TypeScript's instantiation depth limit of 100, causing TS2589. | ||
| 10 | * | ||
| 11 | * Usage: | ||
| 12 | * bun scripts/postcodegen.ts | ||
| 13 | * | ||
| 14 | * Run this after `bunx convex codegen` or `bunx convex dev` (via postinstall). | ||
| 15 | */ | ||
| 16 | |||
| 17 | import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; | ||
| 18 | import { join, resolve } from "node:path"; | ||
| 19 | import * as ts from "typescript"; | ||
| 20 | |||
| 21 | const EXECUTOR_DIR = resolve(import.meta.dirname, ".."); | ||
| 22 | const CONVEX_DIR = join(EXECUTOR_DIR, "convex"); | ||
| 23 | const API_DTS_PATH = join(CONVEX_DIR, "_generated", "api.d.ts"); | ||
| 24 | |||
| 25 | /** Read the generated api.d.ts and extract the module list + components block. */ | ||
| 26 | function parseGeneratedApi(content: string) { | ||
| 27 | const moduleImportRegex = /^import type \* as (\w+) from "\.\.\/(\w+)\.js";$/gm; | ||
| 28 | const modules: { name: string; file: string }[] = []; | ||
| 29 | let match: RegExpExecArray | null; | ||
| 30 | while ((match = moduleImportRegex.exec(content)) !== null) { | ||
| 31 | modules.push({ name: match[1], file: match[2] }); | ||
| 32 | } | ||
| 33 | |||
| 34 | // Extract the components block (everything from `export declare const components:` to the end) | ||
| 35 | const componentsMatch = content.match( | ||
| 36 | /export declare const components:\s*\{[\s\S]*$/, | ||
| 37 | ); | ||
| 38 | const componentsBlock = componentsMatch ? componentsMatch[0] : "export declare const components: {};"; | ||
| 39 | |||
| 40 | return { modules, componentsBlock }; | ||
| 41 | } | ||
| 42 | |||
| 43 | type FunctionKind = "query" | "mutation" | "action"; | ||
| 44 | type FunctionVisibility = "public" | "internal"; | ||
| 45 | |||
| 46 | interface ParsedExport { | ||
| 47 | name: string; | ||
| 48 | kind: FunctionKind; | ||
| 49 | visibility: FunctionVisibility; | ||
| 50 | } | ||
| 51 | |||
| 52 | /** Determine the function kind and visibility from a call expression. */ | ||
| 53 | function resolveRegistration(callName: string): { kind: FunctionKind; visibility: FunctionVisibility } | null { | ||
| 54 | switch (callName) { | ||
| 55 | case "query": return { kind: "query", visibility: "public" }; | ||
| 56 | case "internalQuery": return { kind: "query", visibility: "internal" }; | ||
| 57 | case "mutation": return { kind: "mutation", visibility: "public" }; | ||
| 58 | case "internalMutation": return { kind: "mutation", visibility: "internal" }; | ||
| 59 | case "action": return { kind: "action", visibility: "public" }; | ||
| 60 | case "internalAction": return { kind: "action", visibility: "internal" }; | ||
| 61 | default: return null; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | /** Parse a Convex module file to extract exported function registrations. */ | ||
| 66 | function parseModuleExports(filePath: string): ParsedExport[] { | ||
| 67 | if (!existsSync(filePath)) return []; | ||
| 68 | |||
| 69 | const content = readFileSync(filePath, "utf-8"); | ||
| 70 | const sourceFile = ts.createSourceFile( | ||
| 71 | filePath, | ||
| 72 | content, | ||
| 73 | ts.ScriptTarget.Latest, | ||
| 74 | true, | ||
| 75 | ); | ||
| 76 | |||
| 77 | const exports: ParsedExport[] = []; | ||
| 78 | |||
| 79 | function visit(node: ts.Node) { | ||
| 80 | // Match: export const foo = query({...}) / mutation({...}) / etc. | ||
| 81 | if ( | ||
| 82 | ts.isVariableStatement(node) && | ||
| 83 | node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) | ||
| 84 | ) { | ||
| 85 | for (const decl of node.declarationList.declarations) { | ||
| 86 | if (!ts.isIdentifier(decl.name) || !decl.initializer) continue; | ||
| 87 | const exportName = decl.name.text; | ||
| 88 | |||
| 89 | let callExpr: ts.CallExpression | undefined; | ||
| 90 | |||
| 91 | // Direct call: export const foo = query({...}) | ||
| 92 | if (ts.isCallExpression(decl.initializer)) { | ||
| 93 | callExpr = decl.initializer; | ||
| 94 | } | ||
| 95 | |||
| 96 | // Nullish coalescing fallback: export const foo = bar?.baz ?? internalMutation({...}) | ||
| 97 | // The RHS of ?? determines the Convex registration type. | ||
| 98 | if ( | ||
| 99 | !callExpr && | ||
| 100 | ts.isBinaryExpression(decl.initializer) && | ||
| 101 | decl.initializer.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken | ||
| 102 | ) { | ||
| 103 | const rhs = decl.initializer.right; | ||
| 104 | if (ts.isCallExpression(rhs)) { | ||
| 105 | callExpr = rhs; | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | // Ternary fallback: export const foo = cond ? bar : internalMutation({...}) | ||
| 110 | if ( | ||
| 111 | !callExpr && | ||
| 112 | ts.isConditionalExpression(decl.initializer) | ||
| 113 | ) { | ||
| 114 | const whenFalse = decl.initializer.whenFalse; | ||
| 115 | if (ts.isCallExpression(whenFalse)) { | ||
| 116 | callExpr = whenFalse; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | if (!callExpr) continue; | ||
| 121 | |||
| 122 | // Get the function name being called | ||
| 123 | let calledName: string | undefined; | ||
| 124 | if (ts.isIdentifier(callExpr.expression)) { | ||
| 125 | calledName = callExpr.expression.text; | ||
| 126 | } | ||
| 127 | |||
| 128 | if (!calledName) continue; | ||
| 129 | |||
| 130 | const reg = resolveRegistration(calledName); | ||
| 131 | if (reg) { | ||
| 132 | exports.push({ name: exportName, ...reg }); | ||
| 133 | continue; | ||
| 134 | } | ||
| 135 | |||
| 136 | // Custom builders (workspaceQuery, authedMutation, organizationQuery, etc.) | ||
| 137 | // These wrap standard query/mutation/action builders. We need to determine | ||
| 138 | // which base type they correspond to. | ||
| 139 | const customBuilderMap: Record<string, { kind: FunctionKind; visibility: FunctionVisibility }> = { | ||
| 140 | workspaceQuery: { kind: "query", visibility: "public" }, | ||
| 141 | workspaceMutation: { kind: "mutation", visibility: "public" }, | ||
| 142 | authedQuery: { kind: "query", visibility: "public" }, | ||
| 143 | authedMutation: { kind: "mutation", visibility: "public" }, | ||
| 144 | organizationQuery: { kind: "query", visibility: "public" }, | ||
| 145 | organizationMutation: { kind: "mutation", visibility: "public" }, | ||
| 146 | optionalAccountQuery: { kind: "query", visibility: "public" }, | ||
| 147 | internalOrganizationQuery: { kind: "query", visibility: "internal" }, | ||
| 148 | }; | ||
| 149 | |||
| 150 | const customReg = customBuilderMap[calledName]; | ||
| 151 | if (customReg) { | ||
| 152 | exports.push({ name: exportName, ...customReg }); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | // Match: export default httpRouter() — skip (http module has no function refs) | ||
| 158 | // Match: export { authKitEvent } from ... — already handled above as variable declarations | ||
| 159 | |||
| 160 | ts.forEachChild(node, visit); | ||
| 161 | } | ||
| 162 | |||
| 163 | visit(sourceFile); | ||
| 164 | return exports; | ||
| 165 | } | ||
| 166 | |||
| 167 | /** Group exports by visibility for a module. */ | ||
| 168 | function groupByVisibility(exports: ParsedExport[]) { | ||
| 169 | return { | ||
| 170 | public: exports.filter((e) => e.visibility === "public"), | ||
| 171 | internal: exports.filter((e) => e.visibility === "internal"), | ||
| 172 | }; | ||
| 173 | } | ||
| 174 | |||
| 175 | /** Generate a FunctionReference type string (using `any` for args/returns to avoid deep instantiation). */ | ||
| 176 | function funcRef(kind: FunctionKind, visibility: FunctionVisibility): string { | ||
| 177 | return `FunctionReference<"${kind}", "${visibility}", any, any>`; | ||
| 178 | } | ||
| 179 | |||
| 180 | /** Generate the type block for one namespace (module). */ | ||
| 181 | function generateModuleType(exports: ParsedExport[]): string { | ||
| 182 | if (exports.length === 0) return "Record<string, never>"; | ||
| 183 | const lines = exports.map( | ||
| 184 | (e) => ` ${e.name}: ${funcRef(e.kind, e.visibility)};`, | ||
| 185 | ); | ||
| 186 | return `{\n${lines.join("\n")}\n }`; | ||
| 187 | } | ||
| 188 | |||
| 189 | function main() { | ||
| 190 | // 1. Read the generated api.d.ts | ||
| 191 | if (!existsSync(API_DTS_PATH)) { | ||
| 192 | console.error(`[postcodegen] ${API_DTS_PATH} not found — run 'bunx convex codegen' first`); | ||
| 193 | process.exit(1); | ||
| 194 | } | ||
| 195 | |||
| 196 | const generatedContent = readFileSync(API_DTS_PATH, "utf-8"); | ||
| 197 | const { modules, componentsBlock } = parseGeneratedApi(generatedContent); | ||
| 198 | |||
| 199 | if (modules.length === 0) { | ||
| 200 | console.log("[postcodegen] No modules found in api.d.ts — skipping"); | ||
| 201 | return; | ||
| 202 | } | ||
| 203 | |||
| 204 | // 2. Parse each module's exports | ||
| 205 | const moduleExports = new Map<string, ParsedExport[]>(); | ||
| 206 | for (const mod of modules) { | ||
| 207 | const filePath = join(CONVEX_DIR, `${mod.file}.ts`); | ||
| 208 | const exports = parseModuleExports(filePath); | ||
| 209 | moduleExports.set(mod.name, exports); | ||
| 210 | } | ||
| 211 | |||
| 212 | // 3. Build public and internal API types | ||
| 213 | const publicModules: string[] = []; | ||
| 214 | const internalModules: string[] = []; | ||
| 215 | |||
| 216 | for (const mod of modules) { | ||
| 217 | const exports = moduleExports.get(mod.name) ?? []; | ||
| 218 | const grouped = groupByVisibility(exports); | ||
| 219 | |||
| 220 | if (grouped.public.length > 0) { | ||
| 221 | publicModules.push(` ${mod.name}: ${generateModuleType(grouped.public)};`); | ||
| 222 | } | ||
| 223 | if (grouped.internal.length > 0) { | ||
| 224 | internalModules.push(` ${mod.name}: ${generateModuleType(grouped.internal)};`); | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | // 4. Generate the new api.d.ts | ||
| 229 | const output = `/* eslint-disable */ | ||
| 230 | /** | ||
| 231 | * Generated \`api\` utility. | ||
| 232 | * | ||
| 233 | * THIS CODE IS AUTOMATICALLY GENERATED. | ||
| 234 | * | ||
| 235 | * To regenerate, run \`npx convex dev\`. | ||
| 236 | * | ||
| 237 | * NOTE: This file has been post-processed by scripts/postcodegen.ts | ||
| 238 | * to replace recursive ApiFromModules/FilterApi types with explicit | ||
| 239 | * declarations, avoiding TS2589 depth errors. | ||
| 240 | * | ||
| 241 | * @module | ||
| 242 | */ | ||
| 243 | |||
| 244 | import type { FunctionReference } from "convex/server"; | ||
| 245 | |||
| 246 | /** | ||
| 247 | * A utility for referencing Convex functions in your app's public API. | ||
| 248 | * | ||
| 249 | * Usage: | ||
| 250 | * \`\`\`js | ||
| 251 | * const myFunctionReference = api.myModule.myFunction; | ||
| 252 | * \`\`\` | ||
| 253 | */ | ||
| 254 | export declare const api: { | ||
| 255 | ${publicModules.join("\n")} | ||
| 256 | }; | ||
| 257 | |||
| 258 | /** | ||
| 259 | * A utility for referencing Convex functions in your app's internal API. | ||
| 260 | * | ||
| 261 | * Usage: | ||
| 262 | * \`\`\`js | ||
| 263 | * const myFunctionReference = internal.myModule.myFunction; | ||
| 264 | * \`\`\` | ||
| 265 | */ | ||
| 266 | export declare const internal: { | ||
| 267 | ${internalModules.join("\n")} | ||
| 268 | }; | ||
| 269 | |||
| 270 | ${componentsBlock} | ||
| 271 | `; | ||
| 272 | |||
| 273 | writeFileSync(API_DTS_PATH, output); | ||
| 274 | |||
| 275 | const totalPublic = modules.reduce( | ||
| 276 | (sum, m) => sum + (moduleExports.get(m.name) ?? []).filter((e) => e.visibility === "public").length, | ||
| 277 | 0, | ||
| 278 | ); | ||
| 279 | const totalInternal = modules.reduce( | ||
| 280 | (sum, m) => sum + (moduleExports.get(m.name) ?? []).filter((e) => e.visibility === "internal").length, | ||
| 281 | 0, | ||
| 282 | ); | ||
| 283 | |||
| 284 | console.log( | ||
| 285 | `[postcodegen] Rewrote api.d.ts: ${modules.length} modules, ${totalPublic} public + ${totalInternal} internal function refs`, | ||
| 286 | ); | ||
| 287 | } | ||
| 288 | |||
| 289 | main(); | ||