From 44edc0dc8ad008b3fa603aef0d4faf847869d612 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 30 Sep 2026 18:34:44 -0400 Subject: [PATCH 1/4] Add Stripe Link Eve extension with per-user wallet authorization --- .env.example | 5 + README.md | 34 + agent/extensions/link.ts | 4 + agent/lib/link-auth.ts | 81 + .../_components/authenticated-navigation.tsx | 3 + app/(authenticated)/link/page.tsx | 63 + app/api/link/route.ts | 84 + db/migrations/0014_chief_tigra.sql | 1 + db/migrations/meta/0014_snapshot.json | 2075 +++++++++++++++++ db/migrations/meta/_journal.json | 7 + db/schema/auth.ts | 5 +- db/services/auth/index.ts | 55 + db/services/auth/link.ts | 154 ++ db/tests/database-migration.test.ts | 1 + knip.config.ts | 1 + package.json | 5 +- ...@stripe__link-integrations-eve@0.2.0.patch | 126 + patches/README.md | 24 +- pnpm-lock.yaml | 163 +- pnpm-workspace.yaml | 1 + shared/environment/env.ts | 3 + tests/integration/link.test.ts | 311 +++ tests/turbo-config.test.ts | 2 + turbo.json | 8 + 24 files changed, 3152 insertions(+), 64 deletions(-) create mode 100644 agent/extensions/link.ts create mode 100644 agent/lib/link-auth.ts create mode 100644 app/(authenticated)/link/page.tsx create mode 100644 app/api/link/route.ts create mode 100644 db/migrations/0014_chief_tigra.sql create mode 100644 db/migrations/meta/0014_snapshot.json create mode 100644 db/services/auth/link.ts create mode 100644 patches/@stripe__link-integrations-eve@0.2.0.patch create mode 100644 tests/integration/link.test.ts diff --git a/.env.example b/.env.example index c0ee8587..c16dd29f 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,11 @@ GOOGLE_CONNECTOR_UID= # optional click-to-message shortcut in the workspace. LINQ_CONNECTOR= LINQ_PHONE_NUMBER= +# Optional per-user Link wallet connection. Register the exact callback URL +# https:///api/auth/callback/link with Stripe. +LINK_CLIENT_ID= +LINK_CLIENT_SECRET= +STRIPE_PUBLISHABLE_KEY= # Development benchmarks only (pnpm bench:browser). BROWSER_BENCH_LABEL=self-hosted BROWSER_BENCH_REPETITIONS=1 diff --git a/README.md b/README.md index dca0550f..c01de826 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,40 @@ Gotchas: - Sending email and creating confirmed calendar events always require approval. Calendar events with attendees send Google invitations. +## Link wallet + +The root agent mounts `@stripe/link-integrations-eve` in +`agent/extensions/link.ts`. It uses Stripe's bundled wallet tools, skills, and +approval policies, with per-user authorization through the existing Better Auth +account. It does not use a shared wallet token. + +To enable it, register a Link OAuth client and configure `LINK_CLIENT_ID`, +`LINK_CLIENT_SECRET`, and `STRIPE_PUBLISHABLE_KEY` on the server. Register the +exact redirect URI `https:///api/auth/callback/link` with Stripe, +including a separate localhost URI when developing locally. Set +`BETTER_AUTH_URL` to the canonical application origin. These values are optional +for installations that do not use Link. + +Apply the database migration with `pnpm db:migrate` before enabling Link. It +enforces one Link wallet per OpenInstinct account. Disconnect the current wallet +before connecting a different one; reconnecting the same wallet refreshes its +grant. + +Users connect or disconnect their wallet from **Link wallet** in the sidebar. +An agent request that needs a wallet opens the same connection flow and resumes +through Eve's authorization callback. Connection attempts expire after ten +minutes and belong to the signed-in user. Better Auth stores encrypted grants +and refreshes tokens; disconnection revokes the Link grant before removing it. +Phone sign-in continues to work after disconnecting a wallet. + +Wallet access is available in interactive conversations, not scheduled workers +or scheduled result delivery. The extension's default Eve approval for creating +spend requests remains enabled, separately from approval in Link. Its tools can +return payment credentials into stored Eve tool results; the bundled skills +instruct the agent not to repeat them in chat. Financial-data tools also require +the corresponding Link grant scopes; the default grant requests +`payment_methods.agentic` and `userinfo:read`. + ## Local development The **Deploy with Vercel** flow above is the simplest way to run OpenInstinct. It diff --git a/agent/extensions/link.ts b/agent/extensions/link.ts new file mode 100644 index 00000000..95d3f40f --- /dev/null +++ b/agent/extensions/link.ts @@ -0,0 +1,4 @@ +import link from "@stripe/link-integrations-eve"; +import { linkAuth } from "@agent/lib/link-auth"; + +export default link({ auth: linkAuth }); diff --git a/agent/lib/link-auth.ts b/agent/lib/link-auth.ts new file mode 100644 index 00000000..20ea1945 --- /dev/null +++ b/agent/lib/link-auth.ts @@ -0,0 +1,81 @@ +import { + ConnectionAuthorizationFailedError, + ConnectionAuthorizationRequiredError, + defineInteractiveAuthorization, + type ConnectionPrincipal, +} from "eve/connections"; +import { + consumeLinkAuthorization, + getLinkToken, + LinkConnectionRequiredError, + startLinkAuthorization, +} from "@db/services/auth/link"; +import { applicationOrigin } from "@shared/environment/origin"; +import { scopeFromPrincipal } from "@agent/lib/principal-scope"; + +function accountUserId(principal: ConnectionPrincipal) { + // Scheduled workers and delivery-only reports cannot acquire wallet access. + if ( + principal.type !== "user" || + !principal.id.startsWith("better-auth:") || + principal.attributes?.scheduleId + ) { + throw new ConnectionAuthorizationFailedError("link", { + reason: "principal_required", + retryable: false, + }); + } + scopeFromPrincipal(principal); + return principal.id.slice("better-auth:".length); +} + +async function getToken(principal: ConnectionPrincipal) { + try { + return await getLinkToken(accountUserId(principal)); + } catch (error) { + if (error instanceof LinkConnectionRequiredError) + throw new ConnectionAuthorizationRequiredError("link"); + throw error; + } +} + +export const linkAuth = defineInteractiveAuthorization<{ attempt: string }>({ + displayName: "Link wallet", + getToken: ({ principal }) => getToken(principal), + async startAuthorization({ principal, callbackUrl }) { + const { attempt, expiresAt } = await startLinkAuthorization( + accountUserId(principal), + callbackUrl + ); + return { + challenge: { + url: `${applicationOrigin()}/link?attempt=${attempt}`, + instructions: "Connect your Link wallet to continue.", + expiresAt, + }, + resume: { attempt }, + }; + }, + async completeAuthorization({ principal, callback, resume }) { + if ( + !resume || + callback.params.attempt !== resume.attempt || + !(await consumeLinkAuthorization( + accountUserId(principal), + resume.attempt + )) + ) { + throw new ConnectionAuthorizationFailedError("link", { + reason: "invalid_state", + retryable: false, + }); + } + if (callback.params.error) { + throw new ConnectionAuthorizationFailedError("link", { + reason: "access_denied", + retryable: false, + }); + } + return getToken(principal); + }, +}); diff --git a/app/(authenticated)/_components/authenticated-navigation.tsx b/app/(authenticated)/_components/authenticated-navigation.tsx index 8db5877e..63d0f77f 100644 --- a/app/(authenticated)/_components/authenticated-navigation.tsx +++ b/app/(authenticated)/_components/authenticated-navigation.tsx @@ -7,6 +7,7 @@ import { MessageSquareIcon, PanelsTopLeftIcon, UserRoundIcon, + WalletIcon, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -22,6 +23,7 @@ import { const navigation = [ { href: "/", icon: PanelsTopLeftIcon, id: "workspace", label: "Workspace" }, { href: "/vault", icon: KeyRoundIcon, id: "vault", label: "Vault" }, + { href: "/link", icon: WalletIcon, id: "link", label: "Link wallet" }, { href: "/personal-info", icon: UserRoundIcon, @@ -82,6 +84,7 @@ export function AuthenticatedMobileHeader() { function activeRoute(pathname: string) { if (pathname === "/") return "workspace"; if (pathname.startsWith("/vault")) return "vault"; + if (pathname === "/link") return "link"; if (pathname.startsWith("/personal-info")) return "personal-info"; if (pathname.startsWith("/chat/history")) return "history"; if (pathname.startsWith("/chat")) return "chat"; diff --git a/app/(authenticated)/link/page.tsx b/app/(authenticated)/link/page.tsx new file mode 100644 index 00000000..20cda6e8 --- /dev/null +++ b/app/(authenticated)/link/page.tsx @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { getLinkAccount, linkConfigured } from "@db/services/auth/link"; +import { requireRequestScope } from "@web/auth/request-scope"; +import { Button } from "@web/components/ui/button"; + +export const metadata = { + title: "Link wallet", + referrer: "no-referrer" as const, +}; + +export default async function Page({ searchParams }: PageProps<"/link">) { + const scope = await requireRequestScope(); + const params = await searchParams; + const attempt = z.uuid().safeParse(params.attempt); + const configured = linkConfigured(); + const connected = + configured && + Boolean(await getLinkAccount(scope.userId.slice("better-auth:".length))); + return ( +
+
+

Link wallet

+

+ {configured + ? connected + ? "Your Link wallet is connected. Purchases still require approval in Link." + : "Connect your Link wallet to use it with OpenInstinct. Purchases require your approval in Link." + : "Link is not available on this installation yet."} +

+
+ {params.error && ( +

+ The wallet connection could not be completed. Try again, or sign in + again before disconnecting. To use a different wallet, disconnect the + current wallet first. +

+ )} + {configured && ( +
+ {(!connected || attempt.success) && ( +
+ + {attempt.success && ( + + )} + +
+ )} + {connected && ( +
+ + +
+ )} +
+ )} +
+ ); +} diff --git a/app/api/link/route.ts b/app/api/link/route.ts new file mode 100644 index 00000000..a65e0183 --- /dev/null +++ b/app/api/link/route.ts @@ -0,0 +1,84 @@ +import { z } from "zod"; +import { getAuth } from "@db/services/auth"; +import { getAuthSession } from "@db/services/auth/session"; +import { + disconnectLink, + linkAuthorizationCallback, + linkConfigured, +} from "@db/services/auth/link"; +import { applicationOrigin } from "@shared/environment/origin"; + +const inputSchema = z.object({ + operation: z.enum(["connect", "disconnect"]), + attempt: z.union([z.uuid(), z.literal("")]).optional(), +}); +const privateHeaders = { + "cache-control": "no-store", + "referrer-policy": "no-referrer", +}; + +export async function POST(request: Request) { + if (request.headers.get("origin") !== applicationOrigin()) { + return new Response("Invalid request origin.", { + status: 403, + headers: privateHeaders, + }); + } + const session = await getAuthSession(request.headers); + if (!session) + return new Response("Sign in to continue.", { + status: 401, + headers: privateHeaders, + }); + if (!linkConfigured()) + return new Response("Link is not configured.", { + status: 503, + headers: privateHeaders, + }); + const input = inputSchema.safeParse( + Object.fromEntries(await request.formData()) + ); + if (!input.success) + return new Response("Invalid wallet request.", { + status: 400, + headers: privateHeaders, + }); + + try { + if (input.data.operation === "disconnect") { + await disconnectLink(session.user.id, request.headers); + return new Response(null, { + status: 303, + headers: { ...privateHeaders, location: "/link" }, + }); + } + const callbackURL = input.data.attempt + ? await linkAuthorizationCallback(session.user.id, input.data.attempt) + : new URL("/link", applicationOrigin()).href; + const errorCallback = new URL(callbackURL); + errorCallback.searchParams.set("error", "authorization_failed"); + const auth = await getAuth(); + const result = await auth.api.connectLink({ + body: { callbackURL, errorCallbackURL: errorCallback.href }, + headers: request.headers, + returnHeaders: true, + }); + const headers = new Headers({ + ...privateHeaders, + location: result.response.url, + }); + for (const cookie of result.headers.getSetCookie()) + headers.append("set-cookie", cookie); + return new Response(null, { status: 303, headers }); + } catch { + // Provider failures may carry tokens or upstream response bodies. + const destination = new URL("/link", applicationOrigin()); + destination.searchParams.set("error", "connection_failed"); + if (input.data.attempt) + destination.searchParams.set("attempt", input.data.attempt); + return new Response(null, { + status: 303, + headers: { ...privateHeaders, location: destination.href }, + }); + } +} diff --git a/db/migrations/0014_chief_tigra.sql b/db/migrations/0014_chief_tigra.sql new file mode 100644 index 00000000..e0a04f90 --- /dev/null +++ b/db/migrations/0014_chief_tigra.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "account_link_userId_uidx" ON "account" USING btree ("userId") WHERE "account"."providerId" = 'link'; \ No newline at end of file diff --git a/db/migrations/meta/0014_snapshot.json b/db/migrations/meta/0014_snapshot.json new file mode 100644 index 00000000..bbfc25ee --- /dev/null +++ b/db/migrations/meta/0014_snapshot.json @@ -0,0 +1,2075 @@ +{ + "id": "6917d635-ae89-4aad-a91b-5eb74cdf6c0c", + "prevId": "8d971fcd-e5d8-46d2-93a1-93d6e8ccb63a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_issuer_accountId_uidx": { + "name": "account_issuer_accountId_uidx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accountId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_link_userId_uidx": { + "name": "account_link_userId_uidx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account\".\"providerId\" = 'link'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "phoneNumber": { + "name": "phoneNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phoneNumberVerified": { + "name": "phoneNumberVerified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_phoneNumber_unique": { + "name": "user_phoneNumber_unique", + "nullsNotDistinct": false, + "columns": ["phoneNumber"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.browser_image_artifacts": { + "name": "browser_image_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "root_session_id": { + "name": "root_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_session_id": { + "name": "worker_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_session_id": { + "name": "browser_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_pathname": { + "name": "storage_pathname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "browser_image_artifacts_workspace_idempotency_uidx": { + "name": "browser_image_artifacts_workspace_idempotency_uidx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "browser_image_artifacts_workspace_created_idx": { + "name": "browser_image_artifacts_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_image_artifacts_membership_fkey": { + "name": "browser_image_artifacts_membership_fkey", + "tableFrom": "browser_image_artifacts", + "tableTo": "workspace_memberships", + "columnsFrom": ["workspace_id", "created_by_user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "browser_image_artifacts_status_check": { + "name": "browser_image_artifacts_status_check", + "value": "\"browser_image_artifacts\".\"status\" IN ('pending', 'ready')" + }, + "browser_image_artifacts_source_kind_check": { + "name": "browser_image_artifacts_source_kind_check", + "value": "\"browser_image_artifacts\".\"source_kind\" IN ('element', 'full_page', 'image_resource', 'viewport')" + }, + "browser_image_artifacts_ready_fields_check": { + "name": "browser_image_artifacts_ready_fields_check", + "value": "\"browser_image_artifacts\".\"status\" = 'pending' OR (\"browser_image_artifacts\".\"filename\" IS NOT NULL AND \"browser_image_artifacts\".\"media_type\" IS NOT NULL AND \"browser_image_artifacts\".\"byte_size\" > 0 AND \"browser_image_artifacts\".\"content_hash\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.browser_sessions": { + "name": "browser_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "worker_session_id": { + "name": "worker_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "browser_sessions_workspace_idx": { + "name": "browser_sessions_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "browser_sessions_worker_idx": { + "name": "browser_sessions_worker_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "worker_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_sessions_membership_fkey": { + "name": "browser_sessions_membership_fkey", + "tableFrom": "browser_sessions", + "tableTo": "workspace_memberships", + "columnsFrom": ["workspace_id", "created_by_user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.browser_trace_domains": { + "name": "browser_trace_domains", + "schema": "", + "columns": { + "trace_session_id": { + "name": "trace_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "browser_trace_domains_domain_idx": { + "name": "browser_trace_domains_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_trace_domains_trace_fkey": { + "name": "browser_trace_domains_trace_fkey", + "tableFrom": "browser_trace_domains", + "tableTo": "browser_traces", + "columnsFrom": ["trace_session_id"], + "columnsTo": ["session_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "browser_trace_domains_pkey": { + "name": "browser_trace_domains_pkey", + "columns": ["trace_session_id", "domain"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.browser_trace_events": { + "name": "browser_trace_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trace_session_id": { + "name": "trace_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "browser_trace_events_trace_idx": { + "name": "browser_trace_events_trace_idx", + "columns": [ + { + "expression": "trace_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_trace_events_trace_fkey": { + "name": "browser_trace_events_trace_fkey", + "tableFrom": "browser_trace_events", + "tableTo": "browser_traces", + "columnsFrom": ["trace_session_id"], + "columnsTo": ["session_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.browser_traces": { + "name": "browser_traces", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task": { + "name": "task", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_message": { + "name": "result_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "browser_traces_workspace_started_idx": { + "name": "browser_traces_workspace_started_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_traces_membership_fkey": { + "name": "browser_traces_membership_fkey", + "tableFrom": "browser_traces", + "tableTo": "workspace_memberships", + "columnsFrom": ["workspace_id", "created_by_user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "browser_traces_status_check": { + "name": "browser_traces_status_check", + "value": "\"browser_traces\".\"status\" IN ('running', 'success', 'failure', 'error', 'cancelled')" + }, + "browser_traces_duration_ms_check": { + "name": "browser_traces_duration_ms_check", + "value": "\"browser_traces\".\"duration_ms\" IS NULL OR \"browser_traces\".\"duration_ms\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(16, 8)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chats_workspace_updated_idx": { + "name": "chats_workspace_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chats_workspace_id_fkey": { + "name": "chats_workspace_id_fkey", + "tableFrom": "chats", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chats_input_tokens_check": { + "name": "chats_input_tokens_check", + "value": "\"chats\".\"input_tokens\" >= 0" + }, + "chats_output_tokens_check": { + "name": "chats_output_tokens_check", + "value": "\"chats\".\"output_tokens\" >= 0" + }, + "chats_cost_usd_check": { + "name": "chats_cost_usd_check", + "value": "\"chats\".\"cost_usd\" IS NULL OR \"chats\".\"cost_usd\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.scheduled_agent_jobs": { + "name": "scheduled_agent_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_channel": { + "name": "conversation_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_anchor_message_id": { + "name": "reply_anchor_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timing": { + "name": "timing", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "missed_run_policy": { + "name": "missed_run_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'run_latest'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduled_agent_jobs_due_idx": { + "name": "scheduled_agent_jobs_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_agent_jobs_owner_idx": { + "name": "scheduled_agent_jobs_owner_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "conversation_channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_agent_jobs_membership_fkey": { + "name": "scheduled_agent_jobs_membership_fkey", + "tableFrom": "scheduled_agent_jobs", + "tableTo": "workspace_memberships", + "columnsFrom": ["workspace_id", "created_by_user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduled_agent_jobs_conversation_channel_check": { + "name": "scheduled_agent_jobs_conversation_channel_check", + "value": "\"scheduled_agent_jobs\".\"conversation_channel\" IN ('eve', 'linq')" + }, + "scheduled_agent_jobs_conversation_id_check": { + "name": "scheduled_agent_jobs_conversation_id_check", + "value": "\"scheduled_agent_jobs\".\"conversation_id\" <> ''" + }, + "scheduled_agent_jobs_missed_run_policy_check": { + "name": "scheduled_agent_jobs_missed_run_policy_check", + "value": "\"scheduled_agent_jobs\".\"missed_run_policy\" IN ('skip', 'run_latest', 'catch_up')" + }, + "scheduled_agent_jobs_status_check": { + "name": "scheduled_agent_jobs_status_check", + "value": "\"scheduled_agent_jobs\".\"status\" IN ('active', 'paused', 'completed', 'deleted')" + } + }, + "isRLSEnabled": false + }, + "public.scheduled_agent_runs": { + "name": "scheduled_agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "worker_session_id": { + "name": "worker_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deferred_completion_turn_id": { + "name": "deferred_completion_turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_input_requests": { + "name": "pending_input_requests", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "report_status": { + "name": "report_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_ready'" + }, + "report_sequence": { + "name": "report_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "report_lease_token": { + "name": "report_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "report_lease_expires_at": { + "name": "report_lease_expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduled_agent_runs_occurrence_idx": { + "name": "scheduled_agent_runs_occurrence_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_agent_runs_ready_idx": { + "name": "scheduled_agent_runs_ready_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_agent_runs_report_idx": { + "name": "scheduled_agent_runs_report_idx", + "columns": [ + { + "expression": "report_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_agent_runs_job_id_scheduled_agent_jobs_id_fk": { + "name": "scheduled_agent_runs_job_id_scheduled_agent_jobs_id_fk", + "tableFrom": "scheduled_agent_runs", + "tableTo": "scheduled_agent_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduled_agent_runs_status_check": { + "name": "scheduled_agent_runs_status_check", + "value": "\"scheduled_agent_runs\".\"status\" IN ('queued', 'running', 'waiting_for_input', 'completed', 'dead_letter')" + }, + "scheduled_agent_runs_report_status_check": { + "name": "scheduled_agent_runs_report_status_check", + "value": "\"scheduled_agent_runs\".\"report_status\" IN ('not_ready', 'not_needed', 'pending', 'queued', 'delivered', 'suppressed')" + } + }, + "isRLSEnabled": false + }, + "public.agent_sessions": { + "name": "agent_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_sessions_workspace_idx": { + "name": "agent_sessions_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_sessions_membership_fkey": { + "name": "agent_sessions_membership_fkey", + "tableFrom": "agent_sessions", + "tableTo": "workspace_memberships", + "columnsFrom": ["workspace_id", "created_by_user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_secrets": { + "name": "encrypted_secrets", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_secrets_workspace_id_fkey": { + "name": "encrypted_secrets_workspace_id_fkey", + "tableFrom": "encrypted_secrets", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_secrets_pkey": { + "name": "encrypted_secrets_pkey", + "columns": ["workspace_id", "namespace", "id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "encrypted_secrets_namespace_check": { + "name": "encrypted_secrets_namespace_check", + "value": "\"encrypted_secrets\".\"namespace\" = 'vault'" + } + }, + "isRLSEnabled": false + }, + "public.vault_items": { + "name": "vault_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "vault_items_workspace_updated_idx": { + "name": "vault_items_workspace_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_items_workspace_id_fkey": { + "name": "vault_items_workspace_id_fkey", + "tableFrom": "vault_items", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "vault_items_kind_check": { + "name": "vault_items_kind_check", + "value": "\"vault_items\".\"kind\" IN ('login', 'payment', 'address', 'contact', 'phone', 'identity', 'token')" + } + }, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "settings_workspace_id_fkey": { + "name": "settings_workspace_id_fkey", + "tableFrom": "settings", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "settings_pkey": { + "name": "settings_pkey", + "columns": ["workspace_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "settings_key_check": { + "name": "settings_key_check", + "value": "\"settings\".\"key\" = 'gateway_model'" + } + }, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line_2": { + "name": "address_line_2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_workspace_id_fkey": { + "name": "user_profiles_workspace_id_fkey", + "tableFrom": "user_profiles", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_profiles_country_code_check": { + "name": "user_profiles_country_code_check", + "value": "\"user_profiles\".\"country_code\" IS NULL OR char_length(\"user_profiles\".\"country_code\") = 2" + } + }, + "isRLSEnabled": false + }, + "public.workspace_memberships": { + "name": "workspace_memberships", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_memberships_workspace_id_fkey": { + "name": "workspace_memberships_workspace_id_fkey", + "tableFrom": "workspace_memberships", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_memberships_pkey": { + "name": "workspace_memberships_pkey", + "columns": ["workspace_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_memberships_role_check": { + "name": "workspace_memberships_role_check", + "value": "\"workspace_memberships\".\"role\" = 'owner'" + } + }, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workstreams": { + "name": "workstreams", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_operation_id": { + "name": "last_operation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workstreams_recent_idx": { + "name": "workstreams_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workstreams_workspace_id_workspaces_id_fk": { + "name": "workstreams_workspace_id_workspaces_id_fk", + "tableFrom": "workstreams", + "tableTo": "workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workstreams_workspace_id_scope_key_id_pk": { + "name": "workstreams_workspace_id_scope_key_id_pk", + "columns": ["workspace_id", "scope_key", "id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workstreams_revision_check": { + "name": "workstreams_revision_check", + "value": "\"workstreams\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 32ad7dcf..61cbfdad 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1788879251908, "tag": "0013_last_christian_walker", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1790807210118, + "tag": "0014_chief_tigra", + "breakpoints": true } ] } diff --git a/db/schema/auth.ts b/db/schema/auth.ts index 0a62249f..e2328f77 100644 --- a/db/schema/auth.ts +++ b/db/schema/auth.ts @@ -1,4 +1,4 @@ -import { relations } from "drizzle-orm"; +import { relations, sql } from "drizzle-orm"; import { boolean, index, @@ -80,6 +80,9 @@ export const account = pgTable( table.accountId ), index("account_userId_idx").on(table.userId), + uniqueIndex("account_link_userId_uidx") + .on(table.userId) + .where(sql`${table.providerId} = 'link'`), ] ); diff --git a/db/services/auth/index.ts b/db/services/auth/index.ts index 928775d8..a3180751 100644 --- a/db/services/auth/index.ts +++ b/db/services/auth/index.ts @@ -1,7 +1,9 @@ import { createHash } from "node:crypto"; +import { link } from "@stripe/link-integrations-better-auth"; import { betterAuth } from "better-auth"; import { APIError } from "better-auth/api"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { and, eq } from "drizzle-orm"; import { phoneNumber } from "better-auth/plugins/phone-number"; import { account, db, session, user, verification } from "@db"; import { betterAuthBaseURL } from "@shared/environment/origin"; @@ -28,6 +30,7 @@ async function initializeAuthWithRetry() { async function initializeAuth() { const { betterAuthSecret } = await getInstallationSecrets(); + const { LINK_CLIENT_ID, LINK_CLIENT_SECRET, STRIPE_PUBLISHABLE_KEY } = env; return betterAuth({ appName: "Local Vault Assistant", baseURL: betterAuthBaseURL(), @@ -45,8 +48,60 @@ async function initializeAuth() { "/sign-in/social", "/sign-up/email", "/verify-email", + // Wallet disconnection must revoke the Link grant through its plugin. + "/unlink-account", ], + account: { + encryptOAuthTokens: true, + additionalFields: { + issuer: { + type: "string", + required: true, + input: false, + defaultValue: "better-auth", + }, + }, + accountLinking: { + trustedProviders: ["link"], + allowDifferentEmails: true, + // Phone verification signs users in independently of OAuth accounts. + allowUnlinkingAll: true, + }, + }, + databaseHooks: { + account: { + create: { + before: async (value) => { + if (value.providerId === "link") { + const [existing] = await db + .select({ id: account.id }) + .from(account) + .where( + and( + eq(account.userId, value.userId), + eq(account.providerId, "link") + ) + ) + .limit(1); + // Reauthorization updates the same account. Replacing it requires + // disconnecting first so the previous grant is revoked. + if (existing) return false; + } + return { data: { ...value, issuer: value.providerId } }; + }, + }, + }, + }, plugins: [ + ...(LINK_CLIENT_ID && LINK_CLIENT_SECRET && STRIPE_PUBLISHABLE_KEY + ? [ + link({ + clientId: LINK_CLIENT_ID, + clientSecret: LINK_CLIENT_SECRET, + publishableKey: STRIPE_PUBLISHABLE_KEY, + }), + ] + : []), phoneNumber({ allowedAttempts: 3, expiresIn: 300, diff --git a/db/services/auth/link.ts b/db/services/auth/link.ts new file mode 100644 index 00000000..c599c490 --- /dev/null +++ b/db/services/auth/link.ts @@ -0,0 +1,154 @@ +import { runWithAdapter } from "@better-auth/core/context"; +import { APIError } from "better-auth/api"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { and, eq, gt } from "drizzle-orm"; +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import { account, db, session, user, verification } from "@db"; +import { env } from "@shared/environment"; +import { applicationOrigin } from "@shared/environment/origin"; +import { getAuth } from "@db/services/auth"; + +export function linkConfigured() { + return Boolean( + env.LINK_CLIENT_ID && env.LINK_CLIENT_SECRET && env.STRIPE_PUBLISHABLE_KEY + ); +} + +export class LinkConnectionRequiredError extends Error { + constructor() { + super("Connect your Link wallet to continue."); + this.name = "LinkConnectionRequiredError"; + } +} + +export async function getLinkAccount(userId: string) { + const [result] = await db + .select({ id: account.id }) + .from(account) + .where(and(eq(account.userId, userId), eq(account.providerId, "link"))) + .limit(1); + return result; +} + +// Refresh-token rotation and disconnection share the same database lock. +async function withLinkAccount( + userId: string, + operation: (accountId: string) => Promise +) { + const auth = await getAuth(); + return db.transaction(async (tx) => { + const [saved] = await tx + .select({ id: account.id }) + .from(account) + .where(and(eq(account.userId, userId), eq(account.providerId, "link"))) + .for("update"); + if (!saved) throw new LinkConnectionRequiredError(); + return runWithAdapter( + drizzleAdapter(tx, { + provider: "pg", + schema: { account, session, user, verification }, + })(auth.options), + () => operation(saved.id) + ); + }); +} + +export async function getLinkToken(userId: string) { + if (!linkConfigured()) + throw new Error("Link is not configured on this installation."); + try { + return await withLinkAccount(userId, async (accountId) => { + const auth = await getAuth(); + const result = await auth.api.getAccessToken({ + body: { userId, accountId }, + }); + if (!result.accessToken) throw new LinkConnectionRequiredError(); + return { + token: result.accessToken, + expiresAt: result.accessTokenExpiresAt?.getTime(), + }; + }); + } catch (error) { + if (error instanceof LinkConnectionRequiredError) throw error; + if ( + error instanceof APIError && + (error.statusCode === 400 || error.statusCode === 401) + ) { + throw new LinkConnectionRequiredError(); + } + // oxlint-disable-next-line eslint/preserve-caught-error -- Provider errors can contain tokens and response bodies; keep them out of Eve events. + throw new Error("Link is temporarily unavailable. Try again shortly."); + } +} + +export async function disconnectLink(userId: string, headers: Headers) { + return withLinkAccount(userId, async (accountId) => { + const auth = await getAuth(); + return auth.api.disconnectLink({ body: { accountId }, headers }); + }); +} + +function authorizationIdentifier(userId: string, attempt: string) { + return `link-eve:${JSON.stringify([userId, z.uuid().parse(attempt)])}`; +} + +export async function startLinkAuthorization( + userId: string, + callbackUrl: string +) { + const target = new URL(callbackUrl); + if ( + target.origin !== applicationOrigin() || + !target.pathname.startsWith("/eve/v1/connections/") + ) { + throw new Error("Invalid Link authorization callback."); + } + const attempt = randomUUID(); + target.searchParams.set("attempt", attempt); + const expiresAt = new Date(Date.now() + 10 * 60_000); + await db.insert(verification).values({ + id: randomUUID(), + identifier: authorizationIdentifier(userId, attempt), + value: target.href, + expiresAt, + }); + return { attempt, expiresAt: expiresAt.toISOString() }; +} + +export async function linkAuthorizationCallback( + userId: string, + attempt: string +) { + const [pending] = await db + .select({ value: verification.value }) + .from(verification) + .where( + and( + eq(verification.identifier, authorizationIdentifier(userId, attempt)), + gt(verification.expiresAt, new Date()) + ) + ) + .limit(1); + if (!pending) + throw new Error( + "Link connection expired. Start again from your conversation." + ); + return pending.value; +} + +export async function consumeLinkAuthorization( + userId: string, + attempt: string +) { + const [pending] = await db + .delete(verification) + .where( + and( + eq(verification.identifier, authorizationIdentifier(userId, attempt)), + gt(verification.expiresAt, new Date()) + ) + ) + .returning({ id: verification.id }); + return Boolean(pending); +} diff --git a/db/tests/database-migration.test.ts b/db/tests/database-migration.test.ts index a14e62ed..d4b4ae08 100644 --- a/db/tests/database-migration.test.ts +++ b/db/tests/database-migration.test.ts @@ -41,6 +41,7 @@ describe("database migrations", () => { await applyMigration(database, "0011_faulty_unicorn.sql"); await applyMigration(database, "0012_harsh_domino.sql"); await applyMigration(database, "0013_last_christian_walker.sql"); + await applyMigration(database, "0014_chief_tigra.sql"); const tables = await database.query<{ count: number }>( `SELECT count(*)::int AS count diff --git a/knip.config.ts b/knip.config.ts index 3b59e623..3a85f39a 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -3,6 +3,7 @@ import type { KnipConfig } from "knip"; export default { entry: [ "agent/channels/**/*.ts", + "agent/extensions/**/*.ts", "agent/hooks/**/*.ts", "agent/instructions/**/*.ts", "agent/instrumentation/**/*.ts", diff --git a/package.json b/package.json index 02bdab40..5d3eb8f7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "license": "MIT", "dependencies": { "@base-ui/react": "^1.7.0", + "@better-auth/core": "1.7.5", "@googleapis/calendar": "^16.0.0", "@googleapis/gmail": "^18.0.0", "@googleapis/people": "^8.0.0", @@ -14,6 +15,8 @@ "@streamdown/code": "1.1.1", "@streamdown/math": "1.0.2", "@streamdown/mermaid": "1.0.2", + "@stripe/link-integrations-better-auth": "0.2.0", + "@stripe/link-integrations-eve": "0.2.0", "@t3-oss/env-nextjs": "0.13.11", "@tailwindcss/postcss": "4.3.3", "@tanstack/react-query": "^5.102.6", @@ -24,7 +27,7 @@ "@vercel/connect": "^2.0.0", "@vercel/oidc": "3.8.5", "ai": "^7.0.105", - "better-auth": "1.7.2", + "better-auth": "1.7.5", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", diff --git a/patches/@stripe__link-integrations-eve@0.2.0.patch b/patches/@stripe__link-integrations-eve@0.2.0.patch new file mode 100644 index 00000000..aad09cc4 --- /dev/null +++ b/patches/@stripe__link-integrations-eve@0.2.0.patch @@ -0,0 +1,126 @@ +diff --git a/dist/extension/_manifest.json b/dist/extension/_manifest.json +index 4e60baa523aaa33c5dcec48e92d0912acbcb4862..a3612ee339ec169338e2efe50b03d9840e42c31f 100644 +--- a/dist/extension/_manifest.json ++++ b/dist/extension/_manifest.json +@@ -1,11 +1,11 @@ + { + "kind": "eve-extension", + "formatVersion": 2, +- "builtWithEve": "0.54.4", ++ "builtWithEve": "0.67.2", + "requires": { + "extension": 1, +- "tool": 36, +- "skill": 1, ++ "tool": 58, ++ "skill": 2, + "instructions": 2, + "config": 1 + } +diff --git a/dist/extension/tools/create_report.d.ts b/dist/extension/tools/create_report.d.ts +index 7e6278969032611d5b7b96337734b168a88a26e3..ebc4aa864d2eb7ac0c57d08d1acc2caa94b8b111 100644 +--- a/dist/extension/tools/create_report.d.ts ++++ b/dist/extension/tools/create_report.d.ts +@@ -1,6 +1,6 @@ + declare const _default: import("eve/tools").ToolDefinition<{ + domain: string; +- outcome: "abandoned" | "blocked" | "success"; ++ outcome: "success" | "abandoned" | "blocked"; + spend_request_id: string; + tags?: ("3ds_challenge" | "anti_bot_script" | "captcha" | "cdn_block" | "dns_block" | "login_required" | "other" | "page_inaccessible" | "payment_declined" | "rate_limited" | "site_error" | "stripe_checkout" | "timeout" | "waf_block")[] | undefined; + step?: string | undefined; +@@ -9,7 +9,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + }, import("@stripe/link-sdk").ReportRecord> & { + execute(input: { + domain: string; +- outcome: "abandoned" | "blocked" | "success"; ++ outcome: "success" | "abandoned" | "blocked"; + spend_request_id: string; + tags?: ("3ds_challenge" | "anti_bot_script" | "captcha" | "cdn_block" | "dns_block" | "login_required" | "other" | "page_inaccessible" | "payment_declined" | "rate_limited" | "site_error" | "stripe_checkout" | "timeout" | "waf_block")[] | undefined; + step?: string | undefined; +diff --git a/dist/extension/tools/create_spend_request.d.ts b/dist/extension/tools/create_spend_request.d.ts +index 2017075850f23206b4d48a717f312e07b5428352..bffcfdd3d5cba4e7cb17576d671681f8ea2bf75b 100644 +--- a/dist/extension/tools/create_spend_request.d.ts ++++ b/dist/extension/tools/create_spend_request.d.ts +@@ -1,15 +1,17 @@ + declare const _default: import("eve/tools").ToolDefinition<{ ++ credential_type: "card" | "shared_payment_token"; ++ amount: number; ++ currency: string; ++ context: string; ++ request_approval: boolean; ++ test: boolean; + idempotency_key?: string | undefined; + payment_details?: string | undefined; +- credential_type: "card" | "shared_payment_token"; + network_id?: string | undefined; + execution_method?: "link_pay_token" | undefined; + merchant_account_id?: string | undefined; +- amount: number; +- currency: string; + merchant_name?: string | undefined; + merchant_url?: string | undefined; +- context: string; + line_items?: { + name: string; + quantity?: number | undefined; +@@ -30,22 +32,22 @@ declare const _default: import("eve/tools").ToolDefinition<{ + display_text: string; + amount: number; + }[] | undefined; +- request_approval: boolean; +- test: boolean; + metadata?: Record | undefined; + }, import("@stripe/link-sdk").SpendRequest> & { + execute(input: { ++ credential_type: "card" | "shared_payment_token"; ++ amount: number; ++ currency: string; ++ context: string; ++ request_approval: boolean; ++ test: boolean; + idempotency_key?: string | undefined; + payment_details?: string | undefined; +- credential_type: "card" | "shared_payment_token"; + network_id?: string | undefined; + execution_method?: "link_pay_token" | undefined; + merchant_account_id?: string | undefined; +- amount: number; +- currency: string; + merchant_name?: string | undefined; + merchant_url?: string | undefined; +- context: string; + line_items?: { + name: string; + quantity?: number | undefined; +@@ -66,8 +68,6 @@ declare const _default: import("eve/tools").ToolDefinition<{ + display_text: string; + amount: number; + }[] | undefined; +- request_approval: boolean; +- test: boolean; + metadata?: Record | undefined; + }, ctx: import("eve/tools").ToolContext): Promise; + }; +diff --git a/dist/extension/tools/list_transactions.d.ts b/dist/extension/tools/list_transactions.d.ts +index a286459eb7c19ec256dfc5c7d1c15a206c90aace..85c41bf2dd04ae404a0f17e04e8b9ff05f6fa97c 100644 +--- a/dist/extension/tools/list_transactions.d.ts ++++ b/dist/extension/tools/list_transactions.d.ts +@@ -5,7 +5,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + start_date?: string | undefined; + end_date?: string | undefined; + category?: string | undefined; +- origin?: "external_connection" | "link" | undefined; ++ origin?: "link" | "external_connection" | undefined; + sources?: string[] | undefined; + }, import("@stripe/link-sdk").TransactionsPage> & { + execute(input: { +@@ -15,7 +15,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + start_date?: string | undefined; + end_date?: string | undefined; + category?: string | undefined; +- origin?: "external_connection" | "link" | undefined; ++ origin?: "link" | "external_connection" | undefined; + sources?: string[] | undefined; + }, ctx: import("eve/tools").ToolContext): Promise; + }; diff --git a/patches/README.md b/patches/README.md index c98a9566..c2397490 100644 --- a/patches/README.md +++ b/patches/README.md @@ -1,4 +1,4 @@ -# Eve and Linq compatibility patches +# Dependency compatibility patches Eve is pinned to the published `0.67.2` release. @@ -22,3 +22,25 @@ The application delivery tests exercise that adapter. `defaultTools: false`, waits for child work to start, cancels it, and checks the child's cancellation event. Remove each patch hunk when an upstream release passes its corresponding regression without the hunk. + +`@stripe__link-integrations-eve@0.2.0.patch` contains the generated output of +rebuilding Stripe's official extension source with Eve `0.67.2`. The published +package was built with `0.54.4` and requires tool contract v36, which `0.67.2` +rejects. The compiler emits tool contract v58 and skill contract v2 when it +rebuilds this source. The resulting JavaScript and skills are byte-for-byte +identical to the published package; only generated compatibility metadata and +declaration ordering differ. This is a compiler rebuild, not a manual override +of Eve's compatibility check. + +Source: `stripe/link-cli` commit +`1790c0fa1cc6549f325c74b4af57b06272d5309a` (the 0.2.0 release), directory +`packages/integrations/eve/extension`. To reproduce, copy the published package +to a temporary directory, add that exact upstream `extension/` source, resolve +its dependencies against this application's installed packages, and run +`node /node_modules/eve/bin/eve.js extension build` from the temporary +directory. Replace the patch package's `dist/` with that output and run +`pnpm patch-commit `. + +Remove this patch when a published Stripe extension builds on the pinned Eve +version without it. `pnpm exec eve build --skip-sandbox-prewarm` validates +extension discovery and compilation without requiring a local sandbox daemon. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b037d5..5b195dd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: patchedDependencies: '@linqapp/chat-sdk-adapter@0.5.1': 78b56cdd08c4250ad2680c094858b28d6d6935538d8e59e6dd578f106c77b9e7 + '@stripe/link-integrations-eve@0.2.0': 307b95051f3778ea3fa436732396f804d5cf4bbc8b96192372b6041de15d3517 eve@0.67.2: 43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb importers: @@ -15,6 +16,9 @@ importers: '@base-ui/react': specifier: ^1.7.0 version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@better-auth/core': + specifier: 1.7.5 + version: 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@googleapis/calendar': specifier: ^16.0.0 version: 16.0.0 @@ -48,6 +52,12 @@ importers: '@streamdown/mermaid': specifier: 1.0.2 version: 1.0.2(react@19.2.8) + '@stripe/link-integrations-better-auth': + specifier: 0.2.0 + version: 0.2.0(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)))) + '@stripe/link-integrations-eve': + specifier: 0.2.0 + version: 0.2.0(patch_hash=307b95051f3778ea3fa436732396f804d5cf4bbc8b96192372b6041de15d3517)(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) '@t3-oss/env-nextjs': specifier: 0.13.11 version: 0.13.11(typescript@6.0.3)(zod@4.5.4) @@ -71,7 +81,7 @@ importers: version: 2.8.0 '@vercel/connect': specifier: ^2.0.0 - version: 2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) + version: 2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) '@vercel/oidc': specifier: 3.8.5 version: 3.8.5 @@ -79,8 +89,8 @@ importers: specifier: ^7.0.105 version: 7.0.120(zod@4.5.4) better-auth: - specifier: 1.7.2 - version: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) + specifier: 1.7.5 + version: 1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -509,63 +519,60 @@ packages: '@types/react': optional: true - '@better-auth/core@1.7.2': - resolution: {integrity: sha512-j0nM4ygsWbF/fcYRoKtDn8gn8uLXkmC+075HqSqsJEAV828cJR9bvYBCUQ1zmxNyRBk6Iz/qXsA0Zm2oksiOTg==} + '@better-auth/core@1.7.5': + resolution: {integrity: sha512-kVlSu4H8OKQfjg4b/Zj5MOaospt83N0JbX38wsDzE58Yw95jzovFkU3pxzB1eUFYc4mkuhUMZD8iT1gpUjNMcQ==} peerDependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@cloudflare/workers-types': '>=4' + '@better-fetch/fetch': 1.3.2 '@opentelemetry/api': ^1.9.0 better-call: 1.4.0 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.7.2': - resolution: {integrity: sha512-A5wE10PIv3aS5LGePecEHntQylKy6OOF17B4dqlE0DwJeqU/IOBSd7/LZhMop9cNJ3WFjKMpazVSf91yYM/NFg==} + '@better-auth/drizzle-adapter@1.7.5': + resolution: {integrity: sha512-9SM7v1735SoaedRDcDbHc5ULgXEd2vUlEJkvRHpMF2Q9qf59TRh1b5A9hryyecyi56bm/0CNUDU3nY0uVWj5/Q==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.7.2': - resolution: {integrity: sha512-LYdSRLOvZiF+6S0UThu+wE/Qxsq9P2jQs7ZKkY6BIBJqUjYyxVDmi8HFcantBvWWW1/BeQCSsD7YVDG4gICMIQ==} + '@better-auth/kysely-adapter@1.7.5': + resolution: {integrity: sha512-1wE5gvnjW+c1i4GrLtL9HLn3s0Xrq4YneCDan1NO1dpz0mEguLFNlzfnUGykTFrDwvFh2X5rkIbA8ALCj6WzXQ==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.7.2': - resolution: {integrity: sha512-0q1SXMzm5esH9L0xVuM6IxCk59E4G+3HySX4My9gvEwqtmUobykn+iuc/si3Y4xwUO7JODqQ5o+/pPcLDDMIrA==} + '@better-auth/memory-adapter@1.7.5': + resolution: {integrity: sha512-YDmnfR9zOXbn5SNYYwfHBPuc19hg1d1C1vUXb+Hm8Q91pTsstFNX5OTlZbo7f28q06FpogAEfGGCN/2QV39cig==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.7.2': - resolution: {integrity: sha512-4879SmUWHUs0OYlvHoCFbycZ7i1bqytkcgAUdt9RLQMvZ5H3LRMTgax2YVlGZEXgwNjY/X7xAoXOecWLhlQWeA==} + '@better-auth/mongo-adapter@1.7.5': + resolution: {integrity: sha512-Yq0LfF0VlA9Kfjcjp/v43MSsChv7vKd1ct0Mt15px4zlqaCPIGfPbjw9YNM6jTo0fGpqLR0n15PllSWmLLRp9g==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.7.2': - resolution: {integrity: sha512-mXTr/83WrNWLrvzIjtgDgdu9iXhOcSG1+qBQOAKlbGSFiOB+z4IMRneQ2wmMOiB8mKY9qGkClVUjKRFXqtHnFQ==} + '@better-auth/prisma-adapter@1.7.5': + resolution: {integrity: sha512-QfW6HS9vK0FMcbI/GsQLdplICxOz0EPzYWGONZT4ovL3cSItd4YH/09USbPPVl+VUQCdznAQIk+n+NKvHdmSag==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -575,12 +582,12 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.7.2': - resolution: {integrity: sha512-LcWu+O0zrxYDQj8E36vfkJwGPW4k9ZDA/rCo0zST6ihzL+juR7pBowoZIM9E6tK0Vit52mf6412bGT4XM4eTjQ==} + '@better-auth/telemetry@1.7.5': + resolution: {integrity: sha512-e/REPqMy9Em+gC6G0xWBikiMLRuy532Er7jqdoNkPBa65FbywgWcm1cZbgdW5CnHsrM3OLZsmKn14yeGoDISeg==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} @@ -591,6 +598,9 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -2889,6 +2899,23 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + '@stripe/link-integrations-better-auth@0.2.0': + resolution: {integrity: sha512-bBjqA6cmO1GNV9D6m9pXHaxnm9NR+Nde9poNO9Wn1QapU87xmV93AmUxtXsGHQkrPTIEFTp5W9+X7TQJBT/NSg==} + engines: {node: '>=22'} + peerDependencies: + '@better-auth/core': ^1.7.5 + better-auth: ^1.7.5 + + '@stripe/link-integrations-eve@0.2.0': + resolution: {integrity: sha512-ODNAfSodbtg9HKAjS//BZ/qWTLAcuRJ7Dsp9CL+I+DcDn65YQiVq/MSbDN6D4fIJpLGUmtzfYA+H+KqzhFlKUA==} + engines: {node: '>=24'} + peerDependencies: + eve: '*' + + '@stripe/link-sdk@0.9.0': + resolution: {integrity: sha512-z3qvCYMkPl2JcA2rE3qSy+/uPthI+k3gSGdJIvkf8GHMcy/vG4O70OsWjC71yxyQD/BlyjkpwDvfkdU/Rq4a/g==} + engines: {node: '>=20'} + '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} @@ -3644,15 +3671,14 @@ packages: resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} engines: {node: '>=6.0.0'} - better-auth@1.7.2: - resolution: {integrity: sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A==} + better-auth@1.7.5: + resolution: {integrity: sha512-aKE0Zt2EPTpFvmq4/oATNyG/mAfc6JUqWkW9pzGlrVnzUb0lso7GJ9BPxD6JPhLqYV1a9zOAb0uAz1Q5fm+eHA==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 '@sveltejs/kit': ^2.0.0 '@tanstack/react-start': ^1.0.0 '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 mongodb: ^6.0.0 || ^7.0.0 @@ -3664,7 +3690,7 @@ packages: react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -3677,8 +3703,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -7746,10 +7770,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2)': + '@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2)': dependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@opentelemetry/semantic-conventions': 1.43.0 '@standard-schema/spec': 1.1.0 better-call: 1.4.0(zod@4.5.4) @@ -7760,40 +7784,40 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))': + '@better-auth/drizzle-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: drizzle-orm: 0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0) - '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': + '@better-auth/kysely-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.5 - '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': dependencies: @@ -7805,6 +7829,8 @@ snapshots: '@better-fetch/fetch@1.3.1': {} + '@better-fetch/fetch@1.3.2': {} + '@braintree/sanitize-url@7.1.2': {} '@bytecodealliance/preview2-shim@0.17.6': {} @@ -9398,6 +9424,22 @@ snapshots: mermaid: 11.17.2 react: 19.2.8 + '@stripe/link-integrations-better-auth@0.2.0(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))': + dependencies: + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + better-auth: 1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) + zod: 4.5.4 + + '@stripe/link-integrations-eve@0.2.0(patch_hash=307b95051f3778ea3fa436732396f804d5cf4bbc8b96192372b6041de15d3517)(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4)))': + dependencies: + '@stripe/link-sdk': 0.9.0 + eve: 0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4)) + zod: 4.5.4 + + '@stripe/link-sdk@0.9.0': + dependencies: + zod: 4.5.4 + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -9802,12 +9844,12 @@ snapshots: dependencies: execa: 5.1.1 - '@vercel/connect@2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4)))': + '@vercel/connect@2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4)))': dependencies: '@vercel/oidc': 3.8.5 optionalDependencies: ai: 7.0.120(zod@4.5.4) - better-auth: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) + better-auth: 1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) eve: 0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4)) '@vercel/container@3.0.0(@vercel/build-utils@14.5.0)': @@ -10293,17 +10335,17 @@ snapshots: baseline-browser-mapping@2.11.19: {} - better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))): + better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))): dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) - '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0)) - '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) - '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/drizzle-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0)) + '@better-auth/kysely-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) + '@better-auth/memory-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@noble/ciphers': 2.3.0 '@noble/hashes': 2.3.0 better-call: 1.4.0(zod@4.5.4) @@ -10321,7 +10363,6 @@ snapshots: react-dom: 19.2.8(react@19.2.8) vitest: 4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - '@cloudflare/workers-types' - '@opentelemetry/api' better-call@1.4.0(zod@4.5.4): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7db98eb..4f025225 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,4 +22,5 @@ allowBuilds: sharp: true patchedDependencies: "@linqapp/chat-sdk-adapter@0.5.1": patches/@linqapp__chat-sdk-adapter@0.5.1.patch + "@stripe/link-integrations-eve@0.2.0": patches/@stripe__link-integrations-eve@0.2.0.patch eve@0.67.2: patches/eve@0.67.2.patch diff --git a/shared/environment/env.ts b/shared/environment/env.ts index 06f4f3ad..55630156 100644 --- a/shared/environment/env.ts +++ b/shared/environment/env.ts @@ -85,6 +85,9 @@ export const env = createEnv({ BLOB_STORE_ID: requiredValue.optional(), GOOGLE_CONNECTOR_UID: requiredValue.default("google/open-instinct"), LINQ_CONNECTOR: requiredValue.optional(), + LINK_CLIENT_ID: requiredValue.optional(), + LINK_CLIENT_SECRET: requiredValue.optional(), + STRIPE_PUBLISHABLE_KEY: requiredValue.optional(), LINQ_PHONE_NUMBER: requiredValue .refine( (value) => isE164PhoneNumber(value), diff --git a/tests/integration/link.test.ts b/tests/integration/link.test.ts new file mode 100644 index 00000000..04ad5be9 --- /dev/null +++ b/tests/integration/link.test.ts @@ -0,0 +1,311 @@ +import { PGlite } from "@electric-sql/pglite"; +import { drizzle } from "drizzle-orm/pglite"; +import { eq } from "drizzle-orm"; +import { readFile } from "node:fs/promises"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as schema from "@db/schema"; +import { accessScopeForUser } from "@shared/identity/access-scope"; +import type { ConnectionPrincipal } from "eve/connections"; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.resetModules(); +}); + +describe("Link wallet integration", () => { + it("connects an existing phone user, isolates callbacks, refreshes encrypted tokens, and revokes on disconnect", async () => { + vi.stubEnv("NODE_ENV", "development"); + vi.stubEnv("BETTER_AUTH_URL", "http://localhost:3000"); + vi.stubEnv("LINK_CLIENT_ID", "test-link-client"); + vi.stubEnv("LINK_CLIENT_SECRET", "test-link-secret"); + vi.stubEnv("STRIPE_PUBLISHABLE_KEY", "pk_test_link"); + const client = new PGlite(); + try { + await client.exec( + await readFile( + new URL("../../db/migrations/0001_better-auth.sql", import.meta.url), + "utf8" + ) + ); + await client.exec( + await readFile( + new URL("../../db/migrations/0014_chief_tigra.sql", import.meta.url), + "utf8" + ) + ); + const database = drizzle(client, { schema }); + const Database = await import("@db"); + // SAFETY: PGlite implements the shared Drizzle query-builder and transaction API used by these services. + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Only the database driver changes in this integration test. + vi.spyOn(Database, "db", "get").mockReturnValue(database as never); + const { getAuth } = await import("@db/services/auth"); + const link = await import("@db/services/auth/link"); + const { POST } = await import("@app/api/link/route"); + const { linkAuth } = await import("@agent/lib/link-auth"); + const auth = await getAuth(); + const signedIn = await auth.api.verifyPhoneNumber({ + body: { phoneNumber: "+12025550123", code: "123456" }, + returnHeaders: true, + }); + const headers = new Headers({ + origin: "http://localhost:3000", + cookie: signedIn.headers + .getSetCookie() + .map((cookie) => cookie.split(";")[0]) + .join("; "), + }); + const userId = signedIn.response.user.id; + await expect(link.getLinkToken(userId)).rejects.toThrow( + link.LinkConnectionRequiredError + ); + + const principal = { + type: "user", + id: `better-auth:${userId}`, + issuer: "authjs", + attributes: { + workspaceId: accessScopeForUser(`better-auth:${userId}`).workspaceId, + }, + } satisfies ConnectionPrincipal; + const connection = { url: "" }; + const callbackUrl = + "http://localhost:3000/eve/v1/connections/link/callback/eve-attempt/session:auth"; + await expect( + linkAuth.getToken({ principal, connection }) + ).rejects.toMatchObject({ name: "ConnectionAuthorizationRequiredError" }); + await expect( + linkAuth.getToken({ principal: { type: "app" }, connection }) + ).rejects.toMatchObject({ reason: "principal_required" }); + await expect( + linkAuth.getToken({ + principal: { + ...principal, + attributes: { ...principal.attributes, scheduleId: "scheduled" }, + }, + connection, + }) + ).rejects.toMatchObject({ reason: "principal_required" }); + const authorization = await linkAuth.startAuthorization({ + principal, + connection, + callbackUrl, + }); + if (!authorization.resume) + throw new Error("Expected a resumable Link authorization."); + const pending = authorization.resume; + expect(authorization.challenge.url).toBe( + `http://localhost:3000/link?attempt=${pending.attempt}` + ); + await expect( + link.linkAuthorizationCallback("different-user", pending.attempt) + ).rejects.toThrow("expired"); + await expect( + link.startLinkAuthorization(userId, "https://attacker.example/callback") + ).rejects.toThrow("Invalid"); + const expectedCallback = await link.linkAuthorizationCallback( + userId, + pending.attempt + ); + const request = () => + new Request("http://localhost:3000/api/link", { + method: "POST", + headers, + body: new URLSearchParams({ + operation: "connect", + attempt: pending.attempt, + }), + }); + const denied = request(); + denied.headers.set("origin", "https://attacker.example"); + expect((await POST(denied)).status).toBe(403); + + const started = await POST(request()); + expect(started.status).toBe(303); + const destination = new URL(started.headers.get("location") ?? ""); + expect(destination.origin).toBe("https://login.link.com"); + expect(destination.searchParams.get("redirect_uri")).toBe( + "http://localhost:3000/api/auth/callback/link" + ); + expect(destination.searchParams.get("code_challenge")).toBeTruthy(); + updateCookies(headers, started.headers); + + let linkSubject = "link-user"; + let refreshes = 0; + const tokenRequests: URLSearchParams[] = []; + const revokedTokens: (string | null)[] = []; + let revokeSucceeds = false; + const fetchMock = vi.fn(async (input, init) => { + const outgoing = new Request(input, init); + const url = outgoing.url; + const body = new URLSearchParams(await outgoing.text()); + if (url === "https://login.link.com/auth/token") { + tokenRequests.push(body); + if (body.get("grant_type") === "refresh_token") { + refreshes++; + + return Response.json({ + access_token: "access-refreshed", + refresh_token: "refresh-rotated", + expires_in: 3600, + token_type: "Bearer", + scope: "payment_methods.agentic userinfo:read", + }); + } + + return Response.json({ + access_token: "access-original", + refresh_token: "refresh-original", + expires_in: 3600, + token_type: "Bearer", + scope: "payment_methods.agentic userinfo:read", + }); + } + if (url === "https://api.link.com/userinfo") + return Response.json({ + id: linkSubject, + email: "wallet@example.com", + email_verified: true, + name: "Wallet owner", + }); + if (url === "https://login.link.com/auth/revoke") { + revokedTokens.push(body.get("token")); + return new Response(null, { status: revokeSucceeds ? 200 : 503 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + const callback = new URL("http://localhost:3000/api/auth/callback/link"); + callback.searchParams.set("code", "oauth-code"); + callback.searchParams.set( + "state", + destination.searchParams.get("state") ?? "" + ); + const completed = await auth.handler(new Request(callback, { headers })); + expect(completed.headers.get("location")).toBe(expectedCallback); + const [saved] = await database.select().from(schema.account); + expect(saved).toMatchObject({ + userId, + issuer: "link", + providerId: "link", + accountId: "link-user", + }); + expect(saved?.accessToken).not.toBe("access-original"); + expect(saved?.refreshToken).not.toBe("refresh-original"); + expect((await link.getLinkToken(userId)).token).toBe("access-original"); + await database + .update(schema.account) + .set({ accessTokenExpiresAt: new Date(0) }) + .where(eq(schema.account.userId, userId)); + expect((await link.getLinkToken(userId)).token).toBe("access-refreshed"); + expect(refreshes).toBe(1); + expect(tokenRequests.map((body) => body.get("client_secret"))).toEqual([ + "test-link-secret", + "test-link-secret", + ]); + expect(tokenRequests[0]?.get("code_verifier")).toBeTruthy(); + expect(tokenRequests[1]?.get("refresh_token")).toBe("refresh-original"); + await expect(link.getLinkToken("different-user")).rejects.toThrow( + link.LinkConnectionRequiredError + ); + expect( + await link.consumeLinkAuthorization("different-user", pending.attempt) + ).toBe(false); + const completion = { + principal, + connection, + callbackUrl, + resume: pending, + callback: { method: "GET", params: { attempt: pending.attempt } }, + }; + await expect( + linkAuth.completeAuthorization({ + ...completion, + callback: { method: "GET", params: { attempt: "wrong-attempt" } }, + }) + ).rejects.toMatchObject({ reason: "invalid_state" }); + expect((await linkAuth.completeAuthorization(completion)).token).toBe( + "access-refreshed" + ); + await expect( + linkAuth.completeAuthorization(completion) + ).rejects.toMatchObject({ reason: "invalid_state" }); + expect(await link.consumeLinkAuthorization(userId, pending.attempt)).toBe( + false + ); + const expired = await link.startLinkAuthorization( + userId, + expectedCallback + ); + await database + .update(schema.verification) + .set({ expiresAt: new Date(0) }); + expect(await link.consumeLinkAuthorization(userId, expired.attempt)).toBe( + false + ); + + // A different wallet cannot silently replace or coexist with this one. + linkSubject = "different-link-user"; + const reconnect = await POST( + new Request("http://localhost:3000/api/link", { + method: "POST", + headers, + body: new URLSearchParams({ operation: "connect" }), + }) + ); + const reconnectUrl = new URL(reconnect.headers.get("location") ?? ""); + updateCookies(headers, reconnect.headers); + callback.searchParams.set( + "state", + reconnectUrl.searchParams.get("state") ?? "" + ); + const rejected = await auth.handler(new Request(callback, { headers })); + expect( + new URL(rejected.headers.get("location") ?? "").searchParams.get( + "error" + ) + ).toBe("authorization_failed"); + expect( + await database + .select({ accountId: schema.account.accountId }) + .from(schema.account) + ).toEqual([{ accountId: "link-user" }]); + // The database also enforces this invariant if concurrent callbacks pass the precheck. + await expect( + database.insert(schema.account).values({ + id: "second-account", + issuer: "link", + accountId: "second-wallet", + providerId: "link", + userId, + updatedAt: new Date(), + }) + ).rejects.toMatchObject({ cause: { code: "23505" } }); + + await expect(link.disconnectLink(userId, headers)).rejects.toThrow( + "Unable to revoke Link access" + ); + expect(await link.getLinkAccount(userId)).toBeDefined(); + revokeSucceeds = true; + await link.disconnectLink(userId, headers); + expect(await link.getLinkAccount(userId)).toBeUndefined(); + expect(revokedTokens).toEqual(["refresh-rotated", "refresh-rotated"]); + expect((await auth.api.getSession({ headers }))?.user.id).toBe(userId); + } finally { + await client.close(); + } + }); +}); + +function updateCookies(headers: Headers, response: Headers) { + const cookies = new Map( + [ + ...(headers.get("cookie") ?? "").split("; "), + ...response.getSetCookie().map((cookie) => cookie.split(";")[0] ?? ""), + ] + .filter(Boolean) + .map((cookie) => [cookie.split("=")[0], cookie]) + ); + headers.set("cookie", [...cookies.values()].join("; ")); +} diff --git a/tests/turbo-config.test.ts b/tests/turbo-config.test.ts index 4a758395..fc201299 100644 --- a/tests/turbo-config.test.ts +++ b/tests/turbo-config.test.ts @@ -9,6 +9,8 @@ const applicationEnvironment = [ "*_CONNECTOR_UID", "KERNEL_*", "LINQ_*", + "LINK_*", + "STRIPE_PUBLISHABLE_KEY", "NODE_ENV", "SECRET_ENCRYPTION_KEY", "VERCEL_*", diff --git a/turbo.json b/turbo.json index 129fb85f..ae536e03 100644 --- a/turbo.json +++ b/turbo.json @@ -11,6 +11,8 @@ "*_CONNECTOR_UID", "KERNEL_*", "LINQ_*", + "LINK_*", + "STRIPE_PUBLISHABLE_KEY", "NODE_ENV", "SECRET_ENCRYPTION_KEY", "VERCEL_*" @@ -26,6 +28,8 @@ "*_CONNECTOR_UID", "KERNEL_*", "LINQ_*", + "LINK_*", + "STRIPE_PUBLISHABLE_KEY", "NODE_ENV", "SECRET_ENCRYPTION_KEY", "VERCEL_*" @@ -46,6 +50,8 @@ "*_CONNECTOR_UID", "KERNEL_*", "LINQ_*", + "LINK_*", + "STRIPE_PUBLISHABLE_KEY", "NODE_ENV", "SECRET_ENCRYPTION_KEY", "VERCEL_*" @@ -73,6 +79,8 @@ "*_CONNECTOR_UID", "KERNEL_*", "LINQ_*", + "LINK_*", + "STRIPE_PUBLISHABLE_KEY", "NODE_ENV", "SECRET_ENCRYPTION_KEY", "VERCEL_*" From 3e3c545edbaf59556a6b243753451dacf0d79506 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 30 Sep 2026 18:39:12 -0400 Subject: [PATCH 2/4] Allow CI startup time for Link integration test --- tests/integration/link.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/link.test.ts b/tests/integration/link.test.ts index 04ad5be9..0f9d80f1 100644 --- a/tests/integration/link.test.ts +++ b/tests/integration/link.test.ts @@ -15,6 +15,7 @@ afterEach(() => { }); describe("Link wallet integration", () => { + // Include PGlite startup and the full OAuth lifecycle on slower CI runners. it("connects an existing phone user, isolates callbacks, refreshes encrypted tokens, and revokes on disconnect", async () => { vi.stubEnv("NODE_ENV", "development"); vi.stubEnv("BETTER_AUTH_URL", "http://localhost:3000"); @@ -295,7 +296,7 @@ describe("Link wallet integration", () => { } finally { await client.close(); } - }); + }, 15_000); }); function updateCookies(headers: Headers, response: Headers) { From 5ef4724402459bd0347f2ac2fe8b9253014fb991 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 1 Oct 2026 18:20:42 -0400 Subject: [PATCH 3/4] Migrate Eve to 0.69 resumable tasks and lifecycle events --- README.md | 13 ++ agent/channels/eve.ts | 8 + agent/channels/linq.ts | 8 + agent/hooks/background-reply-target.ts | 12 +- agent/hooks/scheduled-run-completion.ts | 24 +-- .../instructions/content/role/interactive.md | 10 +- .../content/role/scheduled-report.md | 2 +- .../content/worker-coordination.md | 2 +- agent/lib/browser-task.ts | 7 - ...google-workspace-generated-clients.test.ts | 1 + .../browser-agent/lib/trace/timeline.ts | 2 +- agent/tools/no_reply.ts | 14 ++ agent/tools/run_browser.ts | 39 ++-- agent/tools/task_cancel.ts | 1 - .../[sessionId]/_components/activity/card.tsx | 11 +- .../_components/activity/index.tsx | 10 +- .../_components/activity/preview.tsx | 2 +- .../_components/activity/trace.tsx | 4 +- .../_components/conversation/index.test.tsx | 9 +- .../chat/[sessionId]/_lib/trace-view.test.ts | 41 +--- .../chat/[sessionId]/_lib/trace-view.ts | 20 +- app/_lib/subagent-sessions.test.ts | 58 +++--- app/_lib/subagent-sessions.ts | 72 ++++--- evals/agent/conversation.eval.ts | 2 +- evals/agent/routing.eval.ts | 6 +- evals/agent/safety.eval.ts | 8 +- evals/agent/schedules.eval.ts | 2 +- evals/browser/benchmark-activity.ts | 2 +- evals/browser/browser.eval.ts | 23 +-- evals/browser/tests/worker-events.test.ts | 172 +++++----------- evals/browser/worker-events.ts | 148 ++------------ package.json | 2 +- ...@stripe__link-integrations-eve@0.2.0.patch | 6 +- patches/README.md | 29 ++- .../{eve@0.67.2.patch => eve@0.69.0.patch} | 9 - pnpm-lock.yaml | 40 ++-- pnpm-workspace.yaml | 2 +- scripts/run-runtime-smoke.ts | 4 - tests/agent-tool-boundaries.test.ts | 4 +- tests/agent/capabilities.test.ts | 12 +- .../channels/eve-message-delivery.test.ts | 24 +++ .../channels/linq-message-delivery.test.ts | 26 +++ tests/agent/hooks/evlog.test.ts | 3 + .../hooks/scheduled-run-completion.test.ts | 53 +---- tests/agent/hooks/session-owner.test.ts | 3 + tests/agent/reply-targets.test.ts | 20 +- .../hooks/browser-trace-telemetry.test.ts | 3 + tests/agent/tools/google-workspace.test.ts | 1 + tests/agent/tools/run-browser.test.ts | 113 +++++++---- tests/agent/tools/schedules.test.ts | 1 + tests/helpers/tool-context.ts | 1 + tests/runtime/fixture/agent/lib/model.ts | 18 +- tests/runtime/fixture/evals/workflow.eval.ts | 192 ++++++------------ tests/worker-input-bubbling.test.ts | 2 +- 54 files changed, 552 insertions(+), 749 deletions(-) delete mode 100644 agent/lib/browser-task.ts create mode 100644 agent/tools/no_reply.ts delete mode 100644 agent/tools/task_cancel.ts rename patches/{eve@0.67.2.patch => eve@0.69.0.patch} (76%) diff --git a/README.md b/README.md index c01de826..9bbca9ac 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,19 @@ instruct the agent not to repeat them in chat. Financial-data tools also require the corresponding Link grant scopes; the default grant requests `payment_methods.agentic` and `userinfo:read`. +## Eve upgrades + +This application uses Eve `0.69.0`. Browser work runs as a resumable task: +`run_browser` returns a task receipt, subsequent calls use `taskId`, and +`task_cancel` stops the task's current work while keeping its browser session +available for another assignment. Task results arrive within the open turn. + +Eve rejects session handoffs across the 0.69 upgrade boundary. Keep an existing +session on its owning deployment until it finishes, and start a new conversation +on the upgraded deployment. This migration does not cancel or reset existing +production sessions. Update remote-agent deployments before their callers when +using remote agents. + ## Local development The **Deploy with Vercel** flow above is the simplest way to run OpenInstinct. It diff --git a/agent/channels/eve.ts b/agent/channels/eve.ts index aa0c5581..73d4e001 100644 --- a/agent/channels/eve.ts +++ b/agent/channels/eve.ts @@ -71,6 +71,14 @@ const channel = eveChannel({ auth: authenticate, events: { async "action.result"(event, _channel, session) { + if ( + event.status === "completed" && + event.result.kind === "tool-result" && + event.result.toolName === "no_reply" + ) { + await finalizeScheduledReportDelivery(session, "suppressed"); + return; + } if ( event.status === "completed" && sendMessageToolResultSchema.safeParse(event.result).success diff --git a/agent/channels/linq.ts b/agent/channels/linq.ts index 37c0e296..d6a4bc60 100644 --- a/agent/channels/linq.ts +++ b/agent/channels/linq.ts @@ -68,6 +68,14 @@ export default linqChannel({ credentials, events: { async "action.result"(event, context, session) { + if ( + event.status === "completed" && + event.result.kind === "tool-result" && + event.result.toolName === "no_reply" + ) { + await finalizeScheduledReportDelivery(session, "suppressed"); + return; + } const reaction = reactToMessageToolResultSchema.safeParse(event.result); if (event.status === "completed" && reaction.success) { if (!context.thread) { diff --git a/agent/hooks/background-reply-target.ts b/agent/hooks/background-reply-target.ts index f672f6da..5c4ef9e0 100644 --- a/agent/hooks/background-reply-target.ts +++ b/agent/hooks/background-reply-target.ts @@ -1,17 +1,11 @@ import { defineHook } from "eve/hooks"; import { registerBackgroundReplyTarget } from "@agent/lib/reply-targets"; -import { browserTaskReceiptSchema } from "@agent/lib/browser-task"; export default defineHook({ events: { - "action.result"(event, context) { - if (event.data.status !== "completed") return; - const task = browserTaskReceiptSchema.safeParse(event.data.result); - if (!task.success) return; - registerBackgroundReplyTarget( - task.data.output.taskId, - context.session.auth - ); + "task.started"(event, context) { + if (event.data.name !== "run_browser") return; + registerBackgroundReplyTarget(event.data.taskId, context.session.auth); }, }, }); diff --git a/agent/hooks/scheduled-run-completion.ts b/agent/hooks/scheduled-run-completion.ts index 05ba62f8..69a65398 100644 --- a/agent/hooks/scheduled-run-completion.ts +++ b/agent/hooks/scheduled-run-completion.ts @@ -1,10 +1,8 @@ import { defineHook } from "eve/hooks"; -import { browserTaskReceiptSchema } from "@agent/lib/browser-task"; import { scheduledRunIdentity } from "@agent/lib/schedules/identity"; import { scheduledRunOutcomeSchema } from "@shared/schedules/outcome"; import { completeScheduledAgentRun, - deferScheduledAgentRunCompletion, markScheduledAgentRunStarted, releaseScheduledAgentRun, waitForScheduledAgentRunInput, @@ -62,26 +60,6 @@ export default defineHook({ sessionId: ctx.session.id, }); }, - async "action.result"(event, ctx) { - if (event.data.status !== "completed") return; - const task = browserTaskReceiptSchema.safeParse(event.data.result); - if (!task.success) return; - const identity = scheduledRunIdentity(ctx.session.auth); - if (!identity) return; - const deferred = await deferScheduledAgentRunCompletion( - identity.runId, - identity.leaseToken, - ctx.session.turn.id, - new Date(event.meta.at) - ); - console.info("[scheduled-run] worker delegated background work", { - deferred, - runId: identity.runId, - sessionId: ctx.session.id, - taskId: task.data.output.taskId, - turnId: ctx.session.turn.id, - }); - }, async "message.completed"(event, ctx) { const identity = scheduledRunIdentity(ctx.session.auth); if (!identity) return; @@ -102,7 +80,7 @@ export default defineHook({ logDeadLetterReportQueued(status, identity.runId, ctx.session.id); return; } - const message = event.data.message?.trim().slice(0, 4_000); + const message = event.data.message.trim().slice(0, 4_000); const outcome = scheduledRunOutcomeSchema.parse( message ? { diff --git a/agent/instructions/content/role/interactive.md b/agent/instructions/content/role/interactive.md index 9fc676ad..b91066ee 100644 --- a/agent/instructions/content/role/interactive.md +++ b/agent/instructions/content/role/interactive.md @@ -56,15 +56,15 @@ The main conversation is the control plane. Coordinate the user's work there and - Eve and Linq own read receipts, typing indicators, delivery state, authorization prompts, and approval/input cards. Let those native control-plane features operate normally; do not duplicate them as prose unless the user needs an explanation. - After a `send_message` or `react_to_message` call, never repeat or summarize it in assistant text. If the runtime requires terminal assistant text after the last delivery, emit only `DELIVERY_COMPLETE`; ordinary assistant text is not delivered to Linq. - The worker's structured result is coordinator-facing only. Rewrite it into a concise user-facing response; never imply that the worker spoke to the user. -- Start a background worker without a separate preamble. Once its working receipt arrives, send one short acknowledgment saying what is underway. Treat the receipt as acceptance, not completion. -- Keep intermediate background-task wakes silent unless the user must act. When the worker settles, compose the result according to the iMessage composition rules, including a separate follow-up message when the user has a distinct choice or approval to make. -- End the turn whenever the root conversation genuinely needs an answer. Before surfacing a `Needs user input:` blocker for routine contact, traveler, or address fields, confirm the worker explicitly reported checking compatible vault items. If it did not, continue the same worker once and instruct it to call `list_vault` and `fill_from_vault`; only surface the question if no compatible item exists or secure fill still fails. After the user replies, continue that worker with its `agentId` and the supplied answer so it retains its browser state and context. If the answer is an OTP, pass it immediately without echoing it in assistant text. +- Start a browser task without a separate preamble. Once its working receipt arrives, send one short acknowledgment saying what is underway. Treat the receipt as acceptance, not completion. +- Tasks report results within the open turn. Keep intermediate updates silent unless the user must act. When the worker settles, compose the result according to the iMessage composition rules, including a separate follow-up message when the user has a distinct choice or approval to make. +- End the turn whenever the root conversation genuinely needs an answer. Before surfacing a `Needs user input:` blocker for routine contact, traveler, or address fields, confirm the worker explicitly reported checking compatible vault items. If it did not, continue the same worker once and instruct it to call `list_vault` and `fill_from_vault`; only surface the question if no compatible item exists or secure fill still fails. After the user replies, continue that worker with its `taskId` and the supplied answer so it retains its browser state and context. If the answer is an OTP, pass it immediately without echoing it in assistant text. # Worker coordination - Delegate to `browser-agent` only when the task requires interaction with a known site, authenticated or browser-local state, visual inspection, form entry, or content that `web_search` and `web_fetch` could not retrieve. Do not use a generic agent copy or any browser-execution tool yourself. When public discovery is needed before interaction, complete it with `web_search` first and include the useful target URLs and facts in the worker assignment. - Worker images are private, user-scoped artifacts. Ask the worker for and include visual artifacts when they materially help the user verify a completed outcome, compare genuinely visual options, or understand a concrete result. Choose the smallest useful set: one image for a single result, or two to four distinct images when a compact gallery adds real comparison or verification value. Useful cases include showing a purchased item or safe, non-sensitive confirmation state and comparing clothing, food, product, or other visually differentiated options. Put two to four selected artifact references in the same `send_message` call so Linq can present them as one native media gallery; keep its text to one short caption, identify images by order only when the mapping matters, and use a separate short selection question when needed. Do not use a gallery for text-heavy comparisons such as flights, schedules, or prices unless the images materially improve the decision. Skip decorative, repetitive, text-only, or sensitive visuals. Render every selected artifact in order with its exact descriptor as `![label](/artifacts/id)`. Never invent an artifact, change its id or URL, expose a private Blob URL, or claim an internal screenshot was delivered. - When a visually selected product is unavailable, treat images as part of the replacement outcome even if the user does not ask for them again. Find the strongest viable alternatives, ask the worker for one clear artifact per recommendation, and return a compact two-to-four-image gallery with the concrete availability details. -- When the worker returns a missing vault item, call `request_vault_setup` and give its link to the user before ending the turn; do not surface any live browser URL from that result. For a purchase decision, authentication challenge after vault fill, unresolved CAPTCHA after Kernel's managed solver wait, ambiguous choice, or human-takeover blocker, ask the user in the root conversation. Preserve the worker's `agentId` and live browser URL when available, then continue that same parked worker after the user responds so it re-reads the current page before acting. -- Treat a new user message as current steering. Preserve unrelated work. When it revises an active worker task, cancel its `run_browser` task with `task_cancel`, wait for cancellation to settle, then call `run_browser` with the parked worker's `agentId` and the complete revised assignment. Do not send another workflow call to a worker that is still running. Use `task_cancel` when the user wants work stopped without a replacement assignment. Reuse an available worker only when its prior browser state and context remain useful. Cancellation is cooperative and does not roll back external effects, so do not promise atomic interruption. +- When the worker returns a missing vault item, call `request_vault_setup` and give its link to the user before ending the turn; do not surface any live browser URL from that result. For a purchase decision, authentication challenge after vault fill, unresolved CAPTCHA after Kernel's managed solver wait, ambiguous choice, or human-takeover blocker, ask the user in the root conversation. Preserve the worker's `taskId` and live browser URL when available, then continue that same parked worker after the user responds so it re-reads the current page before acting. +- Treat a new user message as current steering. Preserve unrelated work. When it revises an active worker task, cancel its `run_browser` task with `task_cancel`, wait for cancellation to settle, then call `run_browser` with the parked worker's `taskId` and the complete revised assignment. Do not send another workflow call to a worker that is still running. Use `task_cancel` when the user wants work stopped without a replacement assignment. Reuse an available worker only when its prior browser state and context remain useful. Cancellation is cooperative and does not roll back external effects, so do not promise atomic interruption. - Do not create overlapping workers for the same assignment. Do not delegate non-browser work merely to create activity. diff --git a/agent/instructions/content/role/scheduled-report.md b/agent/instructions/content/role/scheduled-report.md index 1bd3c8c0..30048410 100644 --- a/agent/instructions/content/role/scheduled-report.md +++ b/agent/instructions/content/role/scheduled-report.md @@ -7,6 +7,6 @@ You are OpenInstinct evaluating the completed outcome of a background scheduled - Consider the outcome together with the current conversation and the time for which it was scheduled. - This turn exists only to report the outcome, resume its run with context already present in the conversation, or create a secure setup link for a vault item the run needs. Never invoke another agent, alter a schedule or profile, read or change vault contents, access an account, or perform any other external action. - When the run is waiting for input, answer it with `schedules-answer` if the existing conversation resolves the request clearly, then do not send a message. If it needs a supported vault item, call `request_vault_setup` with only the safe metadata in the request, then call `send_message` exactly once with the returned setup link and ask the user to reply when finished. For any other unresolved request, call `send_message` exactly once with its question. Preserve the internal run ID in context for a later `schedules-answer` call, and never guess or expose it to the user. -- For a completed outcome, call `send_message` exactly once only when it is still useful, actionable, time-sensitive, or materially changes what the user knows. Otherwise finish with exactly `` and do not call `send_message`. +- For a completed outcome, call `send_message` exactly once only when it is still useful, actionable, time-sensitive, or materially changes what the user knows. Otherwise call `no_reply` to end the turn silently. - Rewrite useful information as a natural message from OpenInstinct. Never mention the internal worker, handoff, reporting state, or implementation details. - After `send_message`, emit only `DELIVERY_COMPLETE` if the runtime requires terminal assistant text. Never repeat or summarize the delivered message. diff --git a/agent/instructions/content/worker-coordination.md b/agent/instructions/content/worker-coordination.md index 2558ffa4..f4bed94e 100644 --- a/agent/instructions/content/worker-coordination.md +++ b/agent/instructions/content/worker-coordination.md @@ -2,6 +2,6 @@ - Use `run_browser` to give `browser-agent` one bounded browser outcome, the exact target URL when known, all relevant non-secret context, the user's constraints, and any exact transaction approval already granted. The worker does not see the parent conversation. Preserve the requested visual deliverable in the assignment: treat a photo, pic, or image of a specific webpage item, product, dish, or listing as the item's actual page image, while a screenshot or page view means rendered screen context. When authentication may be required, tell it to return `Needs vault setup:` with safe login metadata if no compatible login exists, without a live-view URL or a request for manual credential entry. Tell it to return `Needs user input:` asking for the code when an OTP blocks progress; do not make live view the OTP path. - Begin every worker `message` with `Task: `, followed by a blank line and the complete assignment. Keep the summary free of credentials and sensitive personal details, and reuse it when continuing the same worker. -- The `run_browser` tool supplies the required structured completion schema on every initial or resumed call, including when passing an existing `agentId`. Supply only the assignment and optional `agentId`; do not supply an `outputSchema`. +- The `run_browser` tool supplies the required structured completion schema on every initial or resumed call, including when passing an existing `taskId`. Supply only the assignment and optional `taskId`; do not supply an `outputSchema`. - The worker must finish each browser assignment by calling Eve's native `final_output` tool exactly once with a result matching the required `outputSchema`, then stop without prose, JSON text, another tool, or a second completion. Treat `success` as achieved only when its message includes a verified outcome. Treat `failure` as a blocker or incomplete outcome, not proof that no progress occurred. - Worker images are private, user-scoped artifacts. Ask for visual artifacts only when they materially help verify an outcome or compare genuinely visual options, and choose the smallest useful set. Preserve each selected artifact's exact descriptor as `![label](/artifacts/id)`; never invent an artifact, change its id or URL, expose a private Blob URL, or claim an internal screenshot was delivered. diff --git a/agent/lib/browser-task.ts b/agent/lib/browser-task.ts deleted file mode 100644 index af0382e3..00000000 --- a/agent/lib/browser-task.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const browserTaskReceiptSchema = z.object({ - kind: z.literal("tool-result"), - toolName: z.literal("run_browser"), - output: z.object({ status: z.literal("working"), taskId: z.string().min(1) }), -}); diff --git a/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts b/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts index e062e7ee..e16f7155 100644 --- a/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts +++ b/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts @@ -190,6 +190,7 @@ function toolContext() { async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, + messages: [], abortSignal: new AbortController().signal, callId: "call-1", getToken, diff --git a/agent/subagents/browser-agent/lib/trace/timeline.ts b/agent/subagents/browser-agent/lib/trace/timeline.ts index 1d60aa54..3eeace52 100644 --- a/agent/subagents/browser-agent/lib/trace/timeline.ts +++ b/agent/subagents/browser-agent/lib/trace/timeline.ts @@ -69,7 +69,7 @@ export function traceTimelineRows(event: HookEvent): TraceTimelineRow[] { ]; } case "message.completed": - return [row("Assistant", event.data.message ?? "")]; + return [row("Assistant", event.data.message)]; case "result.completed": return [row("Final output", compactJson(event.data.result))]; case "input.requested": diff --git a/agent/tools/no_reply.ts b/agent/tools/no_reply.ts new file mode 100644 index 00000000..c420cebf --- /dev/null +++ b/agent/tools/no_reply.ts @@ -0,0 +1,14 @@ +import { defineDynamic } from "eve/tools"; +import { noReply } from "eve/tools/no_reply"; +import { resolveModeValue } from "@agent/lib/mode"; + +const noReplyTool = noReply(); + +export default defineDynamic({ + events: { + "turn.started": (_event, context) => + resolveModeValue(context, { + "scheduled-report": { no_reply: noReplyTool }, + }), + }, +}); diff --git a/agent/tools/run_browser.ts b/agent/tools/run_browser.ts index 2cb01ee7..d85d7a89 100644 --- a/agent/tools/run_browser.ts +++ b/agent/tools/run_browser.ts @@ -4,20 +4,31 @@ import { taskCompletionSchema } from "@agent/subagents/browser-agent/lib/complet export default defineWorkflowTool({ description: - "Execute one bounded browser assignment and return a verified result with optional private image artifacts. Pass agentId to continue a parked worker. The completion schema is supplied automatically.", - inputSchema: z.object({ - message: z.string().min(1), - agentId: z.string().optional(), - }), - execution: "background", - async execute(input, ctx) { + "Execute one bounded browser assignment and return a verified result with optional private image artifacts. Continue the same worker using its taskId. The completion schema is supplied automatically.", + inputSchema: z.object({ message: z.string().min(1) }), + async serve(receive, ctx) { "use workflow"; - const result = await ctx.agent("browser-agent", { - ...input, - outputSchema: z - .record(z.string(), z.json()) - .parse(z.toJSONSchema(taskCompletionSchema)), - }); - return taskCompletionSchema.parse(result); + const worker = ctx.agent("browser-agent"); + /* oxlint-disable eslint/no-await-in-loop -- A resumable task serves sequential calls on one child session. */ + for (;;) { + const { input, abortSignal } = await receive(); + try { + const response = await worker.send(input.message, { + outputSchema: taskCompletionSchema, + signal: abortSignal, + }); + const result = await response.result(); + if (abortSignal.aborted) continue; + if (result.status === "failed") { + throw new Error( + result.error?.message ?? "The browser worker failed." + ); + } + ctx.reply(taskCompletionSchema.parse(result.data)); + } catch (error) { + if (!abortSignal.aborted) throw error; + } + } + /* oxlint-enable eslint/no-await-in-loop */ }, }); diff --git a/agent/tools/task_cancel.ts b/agent/tools/task_cancel.ts deleted file mode 100644 index 66342e81..00000000 --- a/agent/tools/task_cancel.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "eve/tools/task_cancel"; diff --git a/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx b/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx index d971e7eb..4ef8b9c0 100644 --- a/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx +++ b/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx @@ -81,20 +81,19 @@ export function ActivityCard({
{sessions.map((session) => { - const status = - statuses.get(session.childSessionId) ?? "starting"; + const status = statuses.get(session.sessionId) ?? "starting"; const task = getSubagentTask( - eventsBySession.get(session.childSessionId) ?? [] + eventsBySession.get(session.sessionId) ?? [] ) ?? session.task; return (