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..384acc6d 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,52 @@ 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`. + +## Eve compatibility + +This branch pins Eve `0.66.3` and the published Link extension `0.2.4`. +The extension's tool contract is supported directly, so it needs no compatibility +rebuild. Browser work uses the background workflow and `agentId` continuation +APIs supported by this Eve version. + +Do not move an active session from Eve `0.69` onto this deployment. Keep existing +sessions on their owning deployment until they finish, and start a new +conversation on this branch's deployment. This rollback does not reset or +cancel production sessions. + ## 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/instructions/content/worker-coordination.md b/agent/instructions/content/worker-coordination.md index 2558ffa4..53744b2e 100644 --- a/agent/instructions/content/worker-coordination.md +++ b/agent/instructions/content/worker-coordination.md @@ -5,3 +5,11 @@ - 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 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. + +# Link purchases + +- Use the official Link extension for wallet access, spend requests, and approvals. Load `link__create-payment-credential` for a purchase or `link__financial-insights` for balances and transactions. Let Eve handle wallet connection; never ask for tokens, card numbers, or security codes, or install the Link CLI. +- For a browser purchase, first have the worker establish the exact merchant URL, items, quantities, options, and final total including tax and shipping. Create a one-time `card` spend request with those details through `link__create_spend_request`, using a stable idempotency key for that purchase. Follow Link's approval URL or required user action, then check the same request with `link__retrieve_spend_request`. Creating a request or receiving a user's chat reply does not establish Link approval. +- Our browser flow retrieves card details only inside `fill_from_link`. Do not request credential expansion, retrieve raw credentials through another tool, or put card details in a worker assignment. After Link reports `approved`, resume the same browser worker with the spend request ID, merchant, items and choices, approved amount in minor currency units, currency, and the user's exact purchase authorization. Tell it to recheck checkout and call `fill_from_link`. +- Shared Payment Tokens, Link Pay Tokens, recurring purchases, and cross-origin payment frames are not supported by this browser bridge. Return the specific limitation instead of inventing a merchant integration or switching payment methods. If Link is unconfigured, direct the user to `/link`; do not claim the wallet is connected. +- A fill is not a completed purchase. Have the worker submit only the authorized checkout and verify a merchant order confirmation. If the result is uncertain, inspect the existing order and spend request; do not create another request or retry the purchase blindly. A changed total or material term requires a corrected request and approval before proceeding. 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/agent/subagents/browser-agent/instructions.md b/agent/subagents/browser-agent/instructions.md index 3abea291..85a1d19e 100644 --- a/agent/subagents/browser-agent/instructions.md +++ b/agent/subagents/browser-agent/instructions.md @@ -23,6 +23,15 @@ You are `browser-agent`, the root coordinator's dedicated browser executor. Comp # Execution +## Link checkout + +- When the coordinator specifies Link, use `fill_from_link` for payment. Do not substitute a saved vault card or ask for payment vault setup. If no approved spend request ID is supplied, preserve the browser and return the exact merchant URL, items, quantities, options, total including tax and shipping, and currency so the coordinator can arrange Link approval. +- With an approved request ID, recheck the purchase and current total, focus a visible card field, and call `fill_from_link` with the browser session ID, spend request ID, observed amount in minor currency units, and lowercase currency. The tool checks the current user's wallet and approved merchant origin. It supports one-time card forms on that origin; stop and report cross-origin payment-frame or unsupported-credential blockers. +- Never inspect, copy, screenshot, or return filled payment values. Check only form validation and non-secret checkout details. The vault retry-once instruction does not apply to Link: after a failed or uncertain fill, report the state without blindly filling again. Return Link connection or approval blockers to the coordinator. +- Filling does not authorize submission. Submit only when the coordinator supplied the user's exact purchase authorization and the checkout still matches. Submit once and verify a merchant order confirmation before reporting success. On a timeout or ambiguous result, preserve the browser and report uncertainty; do not retry the purchase or request another card. + +## Browser operation + - Use `playwright_execute` as the primary browser execution surface. Prefer one bounded program per page state that inspects, performs related safe actions, verifies the meaningful outcome, and returns a compact result. When Playwright is unreliable or semantic interaction is more suitable, inspect with `browser_snapshot`, `browser_text`, or `browser_find`, then use `browser_act` for a short relaxed action plan. `browser_act` dispatches actions and returns the successor state without waiting for model-authored postconditions; do not repeat an action merely because strict causal verification is absent. Use `browser_wait_for` only when the next operation truly depends on a delayed user-visible state. Use current refs only, and snapshot again after navigation, a stale-ref error, or an unavailable successor. - Use `computer_action` only when the page requires visual reasoning or coordinate input that the semantic browser tools cannot express. Never use fixed multi-second sleeps; use `browser_wait_for` with a specific semantic state, URL, title, value, or element condition. - Create one browser and reuse it. Pass a known target as `start_url`. Start read-only; immediately before a saved login is needed, replace it at the same URL with `save_changes: true`, and delete that writer as soon as authentication succeeds so the profile is saved. Only one writable workspace browser may exist. diff --git a/agent/subagents/browser-agent/lib/autofill/native.ts b/agent/subagents/browser-agent/lib/autofill/native.ts index 2eeab78c..59ab04de 100644 --- a/agent/subagents/browser-agent/lib/autofill/native.ts +++ b/agent/subagents/browser-agent/lib/autofill/native.ts @@ -191,6 +191,9 @@ export async function fillWithKernelNativeAutofill({ control.sessionId ); } catch (error) { + // A payment fill may have written values before its response failed. + // Reconcile the checkout instead of retrying another control. + if (kind === "payment") throw error; lastError = error; continue; } diff --git a/agent/subagents/browser-agent/lib/autofill/tests/native-payment.test.ts b/agent/subagents/browser-agent/lib/autofill/tests/native-payment.test.ts new file mode 100644 index 00000000..92a7d664 --- /dev/null +++ b/agent/subagents/browser-agent/lib/autofill/tests/native-payment.test.ts @@ -0,0 +1,163 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; +import { fillWithKernelNativeAutofill } from "../native"; +import { frameOriginExpression } from "../login"; + +vi.mock("@onkernel/sdk", () => ({ + default: class { + browsers = { retrieve: async () => ({ cdp_ws_url: "wss://kernel.test" }) }; + }, +})); + +const commandSchema = z.object({ + id: z.number(), + method: z.string(), + params: z.record(z.string(), z.json()).optional(), + sessionId: z.string().optional(), +}); +const commands: z.infer[] = []; +let pageOrigin = "https://shop.example"; +let frameOrigin = "https://shop.example"; +let failFill = false; + +class BrowserSocket extends EventTarget { + constructor() { + super(); + queueMicrotask(() => this.dispatchEvent(new Event("open"))); + } + close() { + this.dispatchEvent(new Event("close")); + } + send(data: string) { + const command = commandSchema.parse(JSON.parse(data)); + commands.push(command); + let result = {}; + switch (command.method) { + case "Target.getTargets": + result = { + targetInfos: [ + { targetId: "page-1", type: "page", url: `${pageOrigin}/checkout` }, + ], + }; + break; + case "Target.attachToTarget": + result = { sessionId: "cdp-1" }; + break; + case "Page.getFrameTree": + result = { + frameTree: { + frame: { id: "frame-1", url: `${frameOrigin}/checkout` }, + }, + }; + break; + case "Page.createIsolatedWorld": + result = { executionContextId: 1 }; + break; + case "Runtime.evaluate": { + const expression = z.string().parse(command.params?.expression); + if (expression === frameOriginExpression) + result = { result: { value: frameOrigin } }; + else if (expression.includes("flatMap")) + result = { + result: { + value: [ + { autocomplete: "cc-number", focused: true, index: 0 }, + { autocomplete: "cc-csc", focused: false, index: 1 }, + ], + }, + }; + else if (expression.includes("vaultSecret")) + result = { result: { value: 2 } }; + else result = { result: { objectId: "input-1" } }; + break; + } + case "DOM.describeNode": + result = { node: { backendNodeId: 1 } }; + break; + } + queueMicrotask(() => + this.dispatchEvent( + new MessageEvent("message", { + data: JSON.stringify({ + id: command.id, + result, + error: + command.method === "Autofill.trigger" && failFill + ? { message: "unknown fill outcome" } + : undefined, + }), + }) + ) + ); + } +} + +const input = { + browserSessionId: "browser-1", + expectedOrigin: "https://shop.example", + kind: "payment" as const, + claims: Object.entries({ + "cc-name": "Test Buyer", + "cc-number": "4242424242424242", + "cc-csc": "098", + "cc-exp-month": "12", + "cc-exp-year": "2035", + }).map(([token, value]) => ({ id: token, token, value })), +}; +beforeEach(() => { + commands.length = 0; + pageOrigin = "https://shop.example"; + frameOrigin = pageOrigin; + failFill = false; + vi.stubGlobal("WebSocket", BrowserSocket); +}); +afterEach(() => vi.unstubAllGlobals()); + +describe("native payment injection", () => { + it("marks controls before sending card fields through CDP and returns only a receipt", async () => { + const result = await fillWithKernelNativeAutofill(input); + const fillIndex = commands.findIndex( + ({ method }) => method === "Autofill.trigger" + ); + const maskIndex = commands.findIndex(({ params }) => + z.string().safeParse(params?.expression).data?.includes("vaultSecret") + ); + expect(maskIndex).toBeGreaterThan(-1); + expect(maskIndex).toBeLessThan(fillIndex); + expect(commands[fillIndex]?.params?.card).toEqual({ + name: "Test Buyer", + number: "4242424242424242", + cvc: "098", + expiryMonth: "12", + expiryYear: "2035", + }); + expect(result).toEqual({ filledClaims: 5, origin: "https://shop.example" }); + }); + it("rechecks the top-level merchant origin before injection", async () => { + pageOrigin = "https://other.example"; + await expect(fillWithKernelNativeAutofill(input)).rejects.toThrow( + "no longer matches" + ); + expect(commands.some(({ method }) => method === "Autofill.trigger")).toBe( + false + ); + }); + it("does not disclose cards to a cross-origin frame", async () => { + frameOrigin = "https://processor.example"; + await expect(fillWithKernelNativeAutofill(input)).rejects.toThrow( + "No visible form control" + ); + expect(commands.some(({ method }) => method === "Autofill.trigger")).toBe( + false + ); + }); + it("does not try another control after an uncertain payment fill", async () => { + failFill = true; + await expect(fillWithKernelNativeAutofill(input)).rejects.toThrow( + "unknown fill outcome" + ); + expect( + commands.filter(({ method }) => method === "Autofill.trigger") + ).toHaveLength(1); + }); +}); diff --git a/agent/subagents/browser-agent/tools/fill_from_link.ts b/agent/subagents/browser-agent/tools/fill_from_link.ts new file mode 100644 index 00000000..95c3b19f --- /dev/null +++ b/agent/subagents/browser-agent/tools/fill_from_link.ts @@ -0,0 +1,187 @@ +import { + Link, + LinkApiError, + type LinkOptions, + type SpendRequest, +} from "@stripe/link-sdk"; +import { createLinkTools } from "@stripe/link-sdk/tools"; +import { defineTool } from "eve/tools"; +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import { linkAuth } from "@agent/lib/link-auth"; +import { requireWorkerScope } from "../lib/access"; +import { requireOwnedBrowserSession } from "../lib/owned-browser"; +import { + currentKernelPageOrigin, + fillWithKernelNativeAutofill, +} from "../lib/autofill/native"; + +const inputSchema = z.strictObject({ + browserSessionId: z.string().trim().min(1).max(500), + spendRequestId: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/u), + amount: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + currency: z.string().regex(/^[a-z]{3}$/u), +}); + +export default defineTool({ + description: + "Fill a standard card checkout with an approved Link spend request. Supply only its ID, an owned browser session, and the total amount in minor units and lowercase currency just observed at checkout. The tool retrieves credentials for the signed-in user's wallet, verifies approval and merchant origin, and fills them server-side. Never provide or read card details. Focus a card field first. This does not submit a purchase; verify the merchant and total again before submitting. Shared Payment Tokens, Link Pay Tokens, and recurring requests are unsupported here.", + inputSchema, + outputSchema: z.object({ + success: z.literal(true), + spendRequestId: z.string(), + origin: z.string(), + amount: z.number(), + currency: z.string(), + filledClaims: z.number().int().nonnegative(), + }), + async execute(input, context) { + const scope = await requireWorkerScope(context); + await requireOwnedBrowserSession(scope, input.browserSessionId); + // Eve resolves this provider against the current caller, never model input. + const { token: accessToken } = await context.getToken(linkAuth); + const client = new Link({ + accessToken, + fetch(resource, init) { + return fetch(resource, { + ...init, + cache: "no-store", + redirect: "error", + signal: AbortSignal.any([ + context.abortSignal, + AbortSignal.timeout(15_000), + ]), + }); + }, + } satisfies LinkOptions); + + let request: SpendRequest | null; + try { + request = await createLinkTools(client).retrieve_spend_request.execute( + { id: input.spendRequestId, include: ["card"] }, + undefined + ); + } catch (error) { + if (error instanceof LinkApiError && error.status === 401) { + context.requireAuth(linkAuth); + } + // oxlint-disable-next-line eslint/preserve-caught-error -- Link errors can contain credentials; never journal their cause. + throw new Error( + "Could not retrieve this Link request from your wallet. Check its status before retrying." + ); + } + if ( + !request || + request.id !== input.spendRequestId || + request.status !== "approved" + ) { + throw new Error( + "This Link request is not currently approved. Return its ID to the coordinator; do not fill or submit payment." + ); + } + if ( + request.credential_type !== "card" || + request.shared_payment_token || + request.link_pay_token || + request.recurring + ) { + throw new Error( + "This tool supports only one-time Link card requests for standard card forms." + ); + } + if ( + request.amount !== input.amount || + request.currency !== input.currency + ) { + throw new Error( + "The checkout total or currency differs from the approved Link request. Obtain approval for the current purchase before proceeding." + ); + } + const merchant = URL.parse(request.merchant_url ?? ""); + if ( + merchant?.protocol !== "https:" || + merchant.username || + merchant.password + ) { + throw new Error( + "The Link request has no valid HTTPS merchant URL. Ask the coordinator to correct the request." + ); + } + const origin = await currentKernelPageOrigin({ + browserSessionId: input.browserSessionId, + signal: context.abortSignal, + }); + if (origin !== merchant.origin) { + throw new Error( + "The active checkout does not match the approved Link merchant origin. Do not fill this page." + ); + } + + // Validate the fields required by Chromium without returning validation input. + const parsed = z + .object({ + number: z.string().regex(/^\d{12,19}$/u), + cvc: z.string().regex(/^\d{3,4}$/u), + exp_month: z.number().int().min(1).max(12), + exp_year: z.number().int().min(2000).max(2100), + billing_address: z.object({ name: z.string().trim().min(1) }), + valid_until: z.string().optional(), + }) + .safeParse(request.card); + if (!parsed.success) { + throw new Error( + "Link has not supplied complete card details. Return the request ID to the coordinator; never ask for card details in chat." + ); + } + const card = parsed.data; + const now = Date.now(); + const validUntil = + card.valid_until === undefined + ? undefined + : /^\d+(?:\.\d+)?$/u.test(card.valid_until) + ? Number(card.valid_until) * 1000 + : Date.parse(card.valid_until); + if ( + Date.UTC(card.exp_year, card.exp_month) <= now || + (validUntil !== undefined && + (!Number.isFinite(validUntil) || validUntil <= now)) || + (request.expires_at !== undefined && request.expires_at * 1000 <= now) + ) { + throw new Error( + "The Link payment credential or request has expired. Do not fill or submit payment." + ); + } + const claims = ( + [ + ["cc-name", card.billing_address.name], + ["cc-number", card.number], + ["cc-exp-month", String(card.exp_month).padStart(2, "0")], + ["cc-exp-year", String(card.exp_year)], + ["cc-csc", card.cvc], + ] as const + ).map(([token, value]) => ({ id: randomUUID(), token, value })); + + try { + // The existing injector rechecks the origin and masks filled card fields. + const result = await fillWithKernelNativeAutofill({ + browserSessionId: input.browserSessionId, + claims, + expectedOrigin: origin, + kind: "payment", + signal: context.abortSignal, + }); + return { + success: true as const, + spendRequestId: input.spendRequestId, + origin: result.origin, + amount: input.amount, + currency: input.currency, + filledClaims: result.filledClaims, + }; + } catch { + throw new Error( + "Link card autofill could not be confirmed. Check the current checkout without reading payment fields; do not submit or retry blindly." + ); + } + }, +}); diff --git a/agent/tools/link__retrieve_spend_request.ts b/agent/tools/link__retrieve_spend_request.ts new file mode 100644 index 00000000..d0dde912 --- /dev/null +++ b/agent/tools/link__retrieve_spend_request.ts @@ -0,0 +1,42 @@ +import { retrieve_spend_request } from "@stripe/link-integrations-eve/tools"; +import { linkToolSchemas } from "@stripe/link-sdk/tools"; +import { defineTool } from "eve/tools"; + +// Eve's qualified-name override retains the mounted extension's authorization. +export default defineTool({ + description: + "Check a Link spend request's approval status and purchase details without retrieving payment credentials. Pass the approved request ID to browser-agent's fill_from_link for a standard card checkout. Credential expansion is intentionally unavailable to the coordinator.", + inputSchema: linkToolSchemas.retrieveSpendRequest.omit({ include: true }), + async execute({ id }, context) { + const request = await retrieve_spend_request.execute({ id }, context); + if (!request) return null; + if (Symbol.asyncIterator in request) { + throw new Error("Unexpected streaming Link response."); + } + const action = request.status_details?.requires_action?.next_action; + return { + id: request.id, + status: request.status, + amount: request.amount ?? null, + currency: request.currency ?? null, + merchant_name: request.merchant_name ?? null, + merchant_url: request.merchant_url ?? null, + approval_url: request.approval_url ?? null, + activity_url: request.activity_url ?? null, + expires_at: request.expires_at ?? null, + status_details: action + ? { + requires_action: { + next_action: { + type: action.type, + resolution: action.resolution, + display_message: action.display_message, + action_url: action.action_url, + expires_at: action.expires_at ?? null, + }, + }, + } + : null, + }; + }, +}); diff --git a/app/(authenticated)/(workspace)/page.tsx b/app/(authenticated)/(workspace)/page.tsx index f48e7412..064ebbcc 100644 --- a/app/(authenticated)/(workspace)/page.tsx +++ b/app/(authenticated)/(workspace)/page.tsx @@ -4,6 +4,7 @@ import { ImageIcon, MailIcon, MessageSquareIcon, + WalletIcon, } from "lucide-react"; import Link from "next/link"; import type { ReactNode } from "react"; @@ -17,6 +18,7 @@ import { Alert, AlertDescription, AlertTitle } from "@web/components/ui/alert"; import { Badge } from "@web/components/ui/badge"; import { Button } from "@web/components/ui/button"; import { getGatewayModel } from "@db/services/settings"; +import { linkConfigured } from "@db/services/auth/link"; import { env } from "@shared/environment"; import { googleWorkspaceTokenParams } from "@shared/google-workspace/connection"; import { requireRequestScope } from "@web/auth/request-scope"; @@ -54,7 +56,10 @@ export default async function Page({ searchParams }: PageProps<"/">) { linqConfigured={env.LINQ_CONNECTOR !== undefined} linqPhoneNumber={env.LINQ_PHONE_NUMBER} /> - +
@@ -90,10 +95,12 @@ export default async function Page({ searchParams }: PageProps<"/">) { ); } -function GoogleWorkspaceSection({ +function ConnectionsSection({ connection, + linkEnabled, }: { readonly connection?: GoogleWorkspaceConnection; + readonly linkEnabled: boolean; }) { const state = connection?.state; const description = @@ -112,6 +119,27 @@ function GoogleWorkspaceSection({ icon={} label="Google Workspace" /> + } + variant="outline" + aria-label={ + linkEnabled ? "Open Link wallet" : "Set up Link wallet" + } + > + {linkEnabled ? "Open" : "Set up"} + + } + description={ + linkEnabled + ? "Connect or manage your wallet for purchases approved in Link." + : "Set up Link to enable wallet connections." + } + icon={} + label="Link wallet" + />
); 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..5f334c7f --- /dev/null +++ b/app/(authenticated)/link/page.tsx @@ -0,0 +1,141 @@ +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"; +import { applicationOrigin } from "@shared/environment/origin"; + +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 callbackUrl = new URL("/api/auth/callback/link", applicationOrigin()) + .href; + 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."} +

+
+ {!configured && ( +
+ +

+ OpenInstinct works without Link. To enable wallet connections, the + person managing this deployment needs to complete these steps. +

+
    +
  1. +

    + + Register a Link OAuth client with Stripe + + . Include this exact callback URL in the registration: +

    + + {callbackUrl} + +

    + Register each deployment or local development URL you plan to + use. Keep the app’s configured origin consistent with that URL. +

    +
  2. +
  3. +

    + Add these environment variables in your Vercel project’s + Settings, or in .env.local for local development: +

    +
      +
    • + LINK_CLIENT_ID — issued by Stripe +
    • +
    • + LINK_CLIENT_SECRET — issued by Stripe +
    • +
    • + STRIPE_PUBLISHABLE_KEY — from your Stripe account +
    • +
    +

    + Keep the client secret in server settings. Do not paste it into + a conversation or commit it to your repository. +

    +
  4. +
  5. + Redeploy the app, or restart the local server. Return here and + choose Connect Link to authorize your own wallet. +
  6. +
+
+ )} + {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 && ( +
+ + +
+ )} +
+ )} + {configured && ( +
+ +

+ After connecting, ask OpenInstinct to help with a purchase. It will + send you a Link approval request before using payment credentials. + Connecting your wallet does not approve a purchase. +

+

+ You can connect one Link wallet to your OpenInstinct account. To + switch wallets, disconnect the current one first. Disconnecting + revokes OpenInstinct’s access; your phone sign-in still works. +

+
+ )} +
+ ); +} 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..d2f7b217 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,9 @@ "@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.4", + "@stripe/link-sdk": "0.11.0", "@t3-oss/env-nextjs": "0.13.11", "@tailwindcss/postcss": "4.3.3", "@tanstack/react-query": "^5.102.6", @@ -24,13 +28,13 @@ "@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", "credit-card-type": "^10.3.0", "drizzle-orm": "^0.45.2", - "eve": "0.67.2", + "eve": "0.66.3", "evlog": "2.28.1", "lucide-react": "1.34.0", "motion": "13.1.1", diff --git a/patches/README.md b/patches/README.md index c98a9566..7d31005f 100644 --- a/patches/README.md +++ b/patches/README.md @@ -1,12 +1,12 @@ -# Eve and Linq compatibility patches +# Dependency compatibility patches -Eve is pinned to the published `0.67.2` release. +Eve is pinned to the published `0.66.3` release. `@linqapp__chat-sdk-adapter@0.5.1.patch` adds the native reply option to the separately installed adapter while preserving attachments and idempotency. The application delivery tests exercise that adapter. -`eve@0.67.2.patch` preserves native Linq replies and restores task cancellation: +`eve@0.66.3.patch` preserves native Linq replies and restores task cancellation: - The bundled Linq adapter forwards `replyToMessageId` as `reply_to` and uses the installed adapter declarations. The provider-request regression lives in @@ -22,3 +22,8 @@ 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. + +The published `@stripe/link-integrations-eve@0.2.4` declares tool contract v57, +which Eve `0.66.3` supports directly. No Link compatibility patch or local +extension rebuild is needed on this branch. The extension and its Link SDK +`0.11.0` dependency remain installed from npm. diff --git a/patches/eve@0.67.2.patch b/patches/eve@0.66.3.patch similarity index 81% rename from patches/eve@0.67.2.patch rename to patches/eve@0.66.3.patch index a72c90c9..bf923b01 100644 --- a/patches/eve@0.67.2.patch +++ b/patches/eve@0.66.3.patch @@ -23,10 +23,10 @@ index f9d39b06c9250d77adf1875933e39759292ad7a1..ba7ee9dcf278f10887dee7545d3d3865 +`).trim()}function an(e){let t=new Set;for(let n of e){if(n.type===`link`){t.add(n.value);continue}if(n.type===`text`)for(let e of cn(n.value))t.add(e)}return[...t].map(e=>({url:e}))}function on(e){return{type:sn(e.mime_type),url:e.url,name:e.filename,mimeType:e.mime_type,size:e.size_bytes,width:e.width??e.width_px,height:e.height??e.height_px,fetchMetadata:{url:e.url},fetchData:async()=>{let t=await fetch(e.url);if(!t.ok)throw Error(`Failed to fetch Linq attachment ${e.id||e.url}: ${t.status}`);return Buffer.from(await t.arrayBuffer())}}}function sn(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:`file`}function cn(e){return(e.match(/https?:\/\/[^\s<>()]+/gi)??[]).map(e=>e.replace(/[.,!?;:]+$/g,``))}const ln={jpg:`image/jpeg`,jpeg:`image/jpeg`,png:`image/png`,gif:`image/gif`,heic:`image/heic`,heif:`image/heif`,bmp:`image/bmp`,tif:`image/tiff`,tiff:`image/tiff`,webp:`image/webp`,mp4:`video/mp4`,mov:`video/quicktime`,m4v:`video/x-m4v`,"3gp":`video/3gpp`,m4a:`audio/x-m4a`,mp3:`audio/mpeg`,aac:`audio/aac`,wav:`audio/x-wav`,aiff:`audio/x-aiff`,caf:`audio/x-caf`,amr:`audio/amr`,pdf:`application/pdf`,txt:`text/plain`,csv:`text/csv`,rtf:`text/rtf`,vcf:`text/vcard`,ics:`text/calendar`,html:`text/html`,htm:`text/html`,json:`application/json`,zip:`application/zip`,doc:`application/msword`,docx:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,xls:`application/vnd.ms-excel`,xlsx:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,ppt:`application/vnd.ms-powerpoint`,pptx:`application/vnd.openxmlformats-officedocument.presentationml.presentation`};function un(e){return typeof e==`string`||!$(e)?{attachments:[],files:[]}:{attachments:Array.isArray(e.attachments)?e.attachments:[],files:Array.isArray(e.files)?e.files:[]}}async function dn(e,t){let{attachments:n,files:r}=un(t);if(n.length===0&&r.length===0)return[];let i=[];for(let t of n)i.push(await fn(e,t));for(let t of r)i.push(await pn(e,t));return i}async function fn(e,t){if(t.url&&t.url.startsWith(`https://`)&&!_n(t.size))return{type:`media`,url:t.url};let n=await hn(t),r=t.name??bn(t.mimeType);return{type:`media`,attachment_id:await mn(e,n,r,vn(t.mimeType,r))}}async function pn(e,t){let n=await gn(t.data),r=vn(t.mimeType,t.filename);return{type:`media`,attachment_id:await mn(e,n,t.filename,r)}}async function mn(e,t,n,r){let i=await e.attachments.create({filename:n,content_type:r,size_bytes:t.byteLength}),a=await fetch(i.upload_url,{method:i.http_method,headers:i.required_headers,body:t});if(!a.ok)throw Error(`Failed to upload Linq attachment ${n}: ${a.status} ${a.statusText}`);return i.attachment_id}async function hn(e){if(e.data!=null)return gn(e.data);if(typeof e.fetchData==`function`)return gn(await e.fetchData());if(e.url){let t=await fetch(e.url);if(!t.ok)throw Error(`Failed to download Linq attachment ${e.name??e.url}: ${t.status}`);return new Uint8Array(await t.arrayBuffer())}throw Error(`Outbound attachment ${e.name??`(unnamed)`} has no data, fetchData, or url to send`)}async function gn(e){if(e instanceof Uint8Array||e instanceof ArrayBuffer)return new Uint8Array(e);if(typeof Blob<`u`&&e instanceof Blob)return new Uint8Array(await e.arrayBuffer());throw Error(`Unsupported attachment data type; expected Buffer, Blob, or ArrayBuffer`)}function _n(e){return typeof e==`number`&&e>10485760}function vn(e,t){if(e&&e.trim())return yn(e.trim());let n=t.split(`.`).pop()?.toLowerCase(),r=n?ln[n]:void 0;if(r)return r;throw Error(`Cannot determine content type for attachment "${t}"; set mimeType on the attachment`)}function yn(e){let t=e.toLowerCase();return t===`image/jpg`?`image/jpeg`:t}function bn(e){let t=e?xn(e):void 0;return t?`attachment.${t}`:`attachment`}function xn(e){let t=yn(e);for(let[e,n]of Object.entries(ln))if(n===t)return e}const Sn={like:`thumbs_up`,dislike:`thumbs_down`,love:`heart`,laugh:`laugh`,emphasize:`exclamation`,question:`question`};function Cn(e){if(e.reaction_type===`custom`)return e.custom_emoji?{emoji:i.fromGChat(e.custom_emoji),rawEmoji:e.custom_emoji}:null;let t=Sn[e.reaction_type];return t?{emoji:r(t),rawEmoji:e.reaction_type}:null}function wn(e){let t=typeof e==`string`?e:e.name,n=t.trim().replace(/^\{\{emoji:/,``).replace(/\}\}$/,``).replace(/^:+|:+$/g,``).toLowerCase();return[`thumbs_up`,`thumbsup`,`+1`,`like`,`👍`].includes(n)?{type:`like`}:[`thumbs_down`,`thumbsdown`,`-1`,`dislike`,`👎`].includes(n)?{type:`dislike`}:[`heart`,`love`,`❤️`,`❤`].includes(n)?{type:`love`}:[`laugh`,`joy`,`rofl`,`😂`,`🤣`].includes(n)?{type:`laugh`}:[`exclamation`,`emphasize`,`!!`,`!`,`‼️`,`‼`,`❗`].includes(n)?{type:`emphasize`}:[`question`,`?`,`❓`].includes(n)?{type:`question`}:{type:`custom`,custom_emoji:i.toDiscord(t)}}async function Tn(e,t){if(!t)return{ok:!1,response:new Response(`Linq webhook signing secret is not configured`,{status:503})};let n=await e.text();try{new kt.Webhook(t).verify(n,Object.fromEntries(e.headers))}catch{return{ok:!1,response:new Response(`Invalid Linq webhook`,{status:401})}}try{return{ok:!0,event:JSON.parse(n)}}catch{return{ok:!1,response:new Response(`Invalid JSON`,{status:400})}}}const En={"message.sent":`sent`,"message.delivered":`delivered`,"message.read":`read`,"message.failed":`failed`};var Dn=class{name=`linq`;userName=`linq`;apiClient;baseURL;converter=new Jt;credentials;signingSecret;webhookVerifier;chat=null;logger;chatKinds=new Map;deliveryStatusListeners=new Set;constructor(e){if(!e.credentials&&!e.apiKey)throw Error(`Linq requires apiKey or a credentials provider.`);if(!e.webhookVerifier&&!e.credentials&&!e.signingSecret)throw Error(`Linq requires signingSecret or a webhookVerifier.`);this.apiClient=e.apiKey?new Q({apiKey:e.apiKey,baseURL:e.baseURL}):null,this.baseURL=e.baseURL,this.credentials=e.credentials,this.signingSecret=e.signingSecret,this.webhookVerifier=e.webhookVerifier,this.logger=new f}async getApiClient(){if(this.apiClient)return this.apiClient;let e=await this.credentials?.();if(!e?.apiKey)throw Error(`Linq credentials did not provide an API key.`);return new Q({apiKey:e.apiKey,baseURL:this.baseURL})}async getSigningSecret(){if(this.signingSecret)return this.signingSecret;let e=await this.credentials?.();if(!e?.signingSecret)throw Error(`Linq credentials did not provide a webhook signing secret.`);return e.signingSecret}async initialize(e){this.chat=e,this.logger=e.getLogger(`linq`)}encodeThreadId(e){return e.pendingHandle?`linq:pending:${e.pendingHandle}`:(e.isGroup!==void 0&&this.chatKinds.set(e.chatId,e.isGroup),`linq:${e.chatId}`)}decodeThreadId(e){let[t,n,r]=e.split(`:`);if(t!==`linq`||!n)throw Error(`Invalid Linq thread ID: ${e}`);if(n===`pending`){let t=e.slice(13);if(!t)throw Error(`Invalid Linq thread ID: ${e}`);return{chatId:``,pendingHandle:t,isGroup:!1}}if(r===`group`||r===`dm`){let e=r===`group`;return this.chatKinds.set(n,e),{chatId:n,isGroup:e}}if(r!==void 0)throw Error(`Invalid Linq thread ID: ${e}`);return{chatId:n,isGroup:this.chatKinds.get(n)}}async fetchMessages(e,t){let n=this.requireChatId(e),r=await(await this.getApiClient()).chats.messages.list(n,{cursor:t?.cursor,limit:t?.limit});return{messages:r.messages.map(e=>this.parseMessage(e)).sort(function(e,t){return e.metadata.dateSent.getTime()-t.metadata.dateSent.getTime()}),nextCursor:r.next_cursor||void 0}}async fetchMessage(e,t){try{let e=await(await this.getApiClient()).messages.retrieve(t);return this.parseMessage(e)}catch(e){if($(e)&&e.status===404)return null;throw e}}async postMessage(e,t,n={}){let{chatId:r,pendingHandle:i}=this.decodeThreadId(e),{value:a,decorations:o}=Gt(this.converter.renderDecoratedPostable(t)),s=await dn(await this.getApiClient(),t),c=[];if(a){let e=[...o,...n.textDecorations??[]];Kt(e),c.push(e.length?{type:`text`,value:a,text_decorations:e}:{type:`text`,value:a})}let l=Nt(t);if(l){Vt(l)&&this.logger.warn(`Card buttons/selects were flattened to text — onAction() handlers never fire over iMessage/SMS. Use LinkButton/CardLink URLs or handle plain-text replies instead.`);for(let e of Ft(l))c.push({type:`media`,url:e})}if(c.push(...s),c.length===0)throw Error(`Linq message must include text or media.`);let u=await this.getApiClient(),d={...n.idempotencyKey?{idempotency_key:n.idempotencyKey}:{},...n.replyToMessageId?{reply_to:{message_id:n.replyToMessageId}}:{}};if(i){let e=await u.messages.create({to:[i],message:{parts:c,...d}});return{id:e.message.id,threadId:this.encodeThreadId({chatId:e.chat_id}),raw:e}}let f=await u.chats.messages.send(r,{message:{parts:c,...d}});return{id:f.message.id,threadId:this.encodeThreadId({chatId:f.chat_id||r}),raw:f}}requireChatId(e){let{chatId:t,pendingHandle:n}=this.decodeThreadId(e);if(n)throw Error(`Linq thread ${e} has no chat yet — send a message first to create it.`);return t}async openDM(e){let t=e.trim();if(!t)throw Error(`Linq openDM requires a handle.`);return this.encodeThreadId({chatId:``,pendingHandle:t})}async editMessage(e,t,n){let r=this.requireChatId(e),i=this.converter.renderPostable(n).trim();if(!i)throw Error(`Linq message text cannot be empty.`);let a=await(await this.getApiClient()).messages.update(t,{text:i,part_index:0});return{id:a.id,threadId:this.encodeThreadId({chatId:a.chat_id||r}),raw:a}}deleteMessage(e,t){throw new l(`deleteMessage is not implemented`)}async addReaction(e,t,n){await(await this.getApiClient()).messages.addReaction(t,{operation:`add`,...wn(n)})}async removeReaction(e,t,n){await(await this.getApiClient()).messages.addReaction(t,{operation:`remove`,...wn(n)})}async fetchThread(e){let t=this.requireChatId(e),n=await(await this.getApiClient()).chats.retrieve(t);return{id:this.encodeThreadId({chatId:n.id,isGroup:n.is_group}),channelId:this.encodeThreadId({chatId:n.id,isGroup:n.is_group}),channelName:n.display_name??void 0,isDM:!n.is_group,metadata:{chat:n}}}async startTyping(e,t){let n=this.requireChatId(e),{isGroup:r}=this.decodeThreadId(e);if(r!==!0)try{await(await this.getApiClient()).chats.typing.start(n)}catch(e){if($(e)&&e.status===403)return;throw e}}async markRead(e,t){let{chatId:n}=this.decodeThreadId(e);await(await this.getApiClient()).chats.markAsRead(n)}async stream(e,t){let n=``;for await(let e of t){if(typeof e==`string`){n+=e;continue}e.type===`markdown_text`&&(n+=e.text)}return this.postMessage(e,n.trim()?{markdown:n}:` `)}async handleWebhook(e,t){let n=this.webhookVerifier?await this.verifyTrustedWebhook(e):await Tn(e,await this.getSigningSecret());if(!n.ok)return n.response;let{event:r}=n;if(this.chat&&Xt(r)&&r.data.direction===`inbound`){let e=r.data.chat.id,n=r.data.chat.is_group??void 0;if(n===void 0&&!this.chatKinds.has(e))try{let t=await(await this.getApiClient()).chats.retrieve(e);this.chatKinds.set(e,t.is_group)}catch(t){this.logger.warn(`Failed to resolve Linq chat kind for ${e}`,{error:t})}let i=this.encodeThreadId({chatId:e,isGroup:n});this.chat.processMessage(this,i,async()=>this.parseMessage(r.data),t)}else if(this.chat&&Zt(r))this.processReactionWebhook(this.chat,r,t);else{let e=En[r.event_type];e&&this.processDeliveryStatusWebhook(r,e)}return new Response(`OK`,{status:200})}async verifyTrustedWebhook(e){let t=new Uint8Array(await e.arrayBuffer());try{if(await this.webhookVerifier?.(e,t)===!1)return{ok:!1,response:new Response(`Invalid Linq webhook`,{status:401})}}catch{return{ok:!1,response:new Response(`Invalid Linq webhook`,{status:401})}}try{return{ok:!0,event:JSON.parse(new TextDecoder().decode(t))}}catch{return{ok:!1,response:new Response(`Invalid JSON`,{status:400})}}}onDeliveryStatus(e){return this.deliveryStatusListeners.add(e),()=>{this.deliveryStatusListeners.delete(e)}}emitDeliveryStatus(e){for(let t of this.deliveryStatusListeners)try{t(e)}catch(e){this.logger.warn(`Linq delivery-status listener threw`,{error:e})}}processDeliveryStatusWebhook(e,t){if(this.deliveryStatusListeners.size===0)return;let n=e.data,r=n.chat?.id,i=n.id;if(!r||!i){this.logger.debug(`Ignoring Linq ${e.event_type} webhook without chat/message ID`);return}let a={status:t,threadId:this.encodeThreadId({chatId:r}),messageId:i,raw:e,...n.error?{error:n.error}:{}};this.emitDeliveryStatus(a)}processReactionWebhook(e,t,n){let{chat_id:r,message_id:i}=t.data;if(!r||!i){this.logger.debug(`Ignoring Linq ${t.event_type} webhook without chat/message ID`);return}let a=Cn(t.data);if(!a){this.logger.debug(`Ignoring Linq ${t.event_type} webhook with unsupported reaction type ${t.data.reaction_type}`);return}let o=t.data.from_handle,s=t.data.is_from_me||o?.is_me===!0,c=o?.id||o?.handle||t.data.from||`unknown`,l=o?.handle||t.data.from||c;e.processReaction({adapter:this,added:t.event_type===`reaction.added`,emoji:a.emoji,rawEmoji:a.rawEmoji,messageId:i,threadId:this.encodeThreadId({chatId:r}),raw:t,user:{userId:c,userName:l,fullName:l,isBot:s,isMe:s}},n)}parseMessage(e){return Yt(e,e=>this.encodeThreadId(e))}rehydrateAttachment(e){let t=e.fetchMetadata?.url??e.url;return t?{...e,fetchData:async()=>{let e=await fetch(t);if(!e.ok)throw Error(`Failed to fetch Linq attachment ${t}: ${e.status}`);return Buffer.from(await e.arrayBuffer())}}:e}renderFormatted(e){return a(e).trim()}channelIdFromThreadId(e){return e}isDM(e){return this.decodeThreadId(e).isGroup===!1}};function On(e){return new Dn(e)}export{On as createLinqAdapter}; \ No newline at end of file diff --git a/dist/src/execution/node-step.js b/dist/src/execution/node-step.js -index 3cb5ec046e87395260ec9979c43ca37f65e8db00..75146d078aa693b76aff360bc633a6ce48a24d8d 100644 +index 69c199569bcf5d2329ac8c2fb844e6f5ec408f4b..76ff78809ab7c2bad98869017b0be4eef1ed3fd4 100644 --- a/dist/src/execution/node-step.js +++ b/dist/src/execution/node-step.js @@ -1 +1 @@ --import{createLogger}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{UNSPECIFIED_INPUT_SCHEMA}from"#tools/schema.js";import{createToolExecuteWithAuth}from"#execution/tool-auth.js";import{resolveRuntimeModelReference}from"#runtime/agent/resolve-model.js";import{createPreparedWorkflowToolHarnessDefinition,createWorkflowToolHarnessDefinition}from"#execution/tools/workflow/background.js";import{LOAD_SKILL_TOOL_NAME}from"#runtime/skills/fragment-context.js";import{dispatchDynamicModelEvent}from"#context/dynamic-model-lifecycle.js";import{preparePersistedStepDynamicToolMetadata}from"#context/dynamic-tool-lifecycle.js";import{createToolLoopHarness}from"#harness/tool-loop.js";import{createTaskToolHarnessDefinitions}from"#execution/tools/tasks.js";import{findRegisteredRuntimeTool}from"#runtime/tools/registry.js";import{connectionToolReplayIdentity}from"#execution/tools/connection-search.js";import{WEB_SEARCH_TOOL_NAME,resolveWebSearchActivityLabel}from"#harness/provider-tool-schemas.js";const log=createLogger(`execution.node-step`);function createExecutionNodeStep(e){let t=createRuntimeModelResolver(e.modelResolutionScope),n=e.node.turnAgent.dynamicModel===void 0?void 0:createRuntimeDynamicModelEventDispatcher(e.modelResolutionScope,e.node.turnAgent.dynamicModel,e.abortSignal),r=createNodeHarnessTools({node:e.node}),i=e.instrumentation,a=i?.prepareExecution(),o=createToolLoopHarness({steeringSignal:e.steeringSignal,abortSignal:e.abortSignal,capabilities:e.capabilities,clearOnly:e.clearOnly,compactOnly:e.compactOnly,handleEvent:e.handleEvent,historyProjector:e.historyProjector,historyView:e.historyView,instrumentation:a,prepareApprovalTurn:e.prepareApprovalTurn,toolReplayIdentity:connectionToolReplayIdentity,resolveStepDynamicTools:t=>preparePersistedStepDynamicToolMetadata({...t,resolvers:e.node.agent.dynamicToolResolvers??[]}),dispatchDynamicModelEvent:n,resolveModel:t,runtimeIdentity:buildRuntimeIdentity(e.node),tools:r});return i===void 0?o:async(e,t)=>{try{return await o(e,t)}finally{await i.flush()}}}function buildRuntimeIdentity(e){let t=resolveInstalledPackageInfo(),n={agentId:e.turnAgent.id,agentName:e.agent.config?.name,eveVersion:t.version},r=process.env.VERCEL_GIT_COMMIT_SHA?.trim(),i=process.env.VERCEL_GIT_COMMIT_REF?.trim(),a=process.env.VERCEL_DEPLOYMENT_CREATED_AT?.trim();return r||i||a?{...n,build:{deployedAt:a||void 0,gitBranch:i||void 0,gitSha:r||void 0}}:n}function createRuntimeModelResolver(e){return t=>resolveRuntimeModelReference(t,e)}function createRuntimeDynamicModelEventDispatcher(e,t,n){return r=>dispatchDynamicModelEvent({abortSignal:n,ctx:r.ctx,dynamicModel:t,event:r.event,messages:r.messages,scope:e})}function createNodeHarnessTools(e){let t=new Map;for(let n of e.node.turnAgent.tools){let r=resolveHarnessToolDefinition({node:e.node,tool:n});r!==null&&t.set(n.name,r)}return t}function resolveHarnessToolDefinition(e){let t=findRegisteredRuntimeTool(e.node.toolRegistry,e.tool.name);return isPreparedRuntimeWorkflowTool(e.tool)?t===null?createPreparedWorkflowToolHarnessDefinition(e.tool):createWorkflowToolHarnessDefinition({executeInput:t.definition.executeInput,definition:createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly}),nodeId:e.tool.task.nodeId,workflowId:e.tool.task.workflowId}):t===null?(log.warn(`declared tool is not registered — omitting from toolset`,{toolName:e.tool.name,nodeId:e.node.nodeId}),null):createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly})}function isPreparedRuntimeWorkflowTool(e){return e.task!==void 0}function createRegisteredHarnessToolDefinition(e){let t=e.definition;if(t.owner.kind===`framework`){let n=createTaskToolHarnessDefinitions().find(e=>e.name===t.name);if(n!==void 0)return{...n,behavior:e.behavior}}let n=t.execute;return{availableInSubagents:t.availableInSubagents,label:t.label??(t.owner.kind===`framework`&&t.name===WEB_SEARCH_TOOL_NAME?{start:resolveWebSearchActivityLabel}:void 0),approvalKey:t.approvalKey,behavior:e.behavior,description:t.description,execution:t.execution,executeInput:t.executeInput,execute:resolveAuthoredExecute({rawExecute:n,scope:t.name}),frameworkAction:t.owner.kind===`framework`&&t.name===LOAD_SKILL_TOOL_NAME?`load-skill`:void 0,inputSchema:t.inputSchema??UNSPECIFIED_INPUT_SCHEMA,name:t.name,approval:t.approval,outputSchema:t.outputSchema,rootOnly:e.rootOnly,toModelOutput:t.toModelOutput}}function resolveAuthoredExecute(e){let{rawExecute:t,scope:n}=e;return t===void 0?void 0:createToolExecuteWithAuth({execute:t,scope:n})}export{buildRuntimeIdentity,createExecutionNodeStep,createNodeHarnessTools}; +-import{createLogger}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{UNSPECIFIED_INPUT_SCHEMA}from"#tools/schema.js";import{createToolExecuteWithAuth}from"#execution/tool-auth.js";import{resolveRuntimeModelReference}from"#runtime/agent/resolve-model.js";import{createPreparedWorkflowToolHarnessDefinition,createWorkflowToolHarnessDefinition}from"#execution/tools/workflow/background.js";import{LOAD_SKILL_TOOL_NAME}from"#runtime/skills/fragment-context.js";import{dispatchDynamicModelEvent}from"#context/dynamic-model-lifecycle.js";import{preparePersistedStepDynamicToolMetadata}from"#context/dynamic-tool-lifecycle.js";import{createToolLoopHarness}from"#harness/tool-loop.js";import{createTaskToolHarnessDefinitions}from"#execution/tools/tasks.js";import{findRegisteredRuntimeTool}from"#runtime/tools/registry.js";import{WEB_SEARCH_TOOL_NAME,resolveWebSearchActivityLabel}from"#harness/provider-tool-schemas.js";const log=createLogger(`execution.node-step`);function createExecutionNodeStep(e){let t=createRuntimeModelResolver(e.modelResolutionScope),n=e.node.turnAgent.dynamicModel===void 0?void 0:createRuntimeDynamicModelEventDispatcher(e.modelResolutionScope,e.node.turnAgent.dynamicModel,e.abortSignal),r=createNodeHarnessTools({node:e.node}),i=e.instrumentation,a=i?.prepareExecution(),o=createToolLoopHarness({steeringSignal:e.steeringSignal,abortSignal:e.abortSignal,capabilities:e.capabilities,clearOnly:e.clearOnly,compactOnly:e.compactOnly,handleEvent:e.handleEvent,historyProjector:e.historyProjector,historyView:e.historyView,instrumentation:a,mode:e.mode,resolveStepDynamicTools:t=>preparePersistedStepDynamicToolMetadata({...t,resolvers:e.node.agent.dynamicToolResolvers??[]}),dispatchDynamicModelEvent:n,resolveModel:t,runtimeIdentity:buildRuntimeIdentity(e.node),tools:r});return i===void 0?o:async(e,t)=>{try{return await o(e,t)}finally{await i.flush()}}}function buildRuntimeIdentity(e){let t=resolveInstalledPackageInfo(),n={agentId:e.turnAgent.id,agentName:e.agent.config?.name,eveVersion:t.version},r=process.env.VERCEL_GIT_COMMIT_SHA?.trim(),i=process.env.VERCEL_GIT_COMMIT_REF?.trim(),a=process.env.VERCEL_DEPLOYMENT_CREATED_AT?.trim();return r||i||a?{...n,build:{deployedAt:a||void 0,gitBranch:i||void 0,gitSha:r||void 0}}:n}function createRuntimeModelResolver(e){return t=>resolveRuntimeModelReference(t,e)}function createRuntimeDynamicModelEventDispatcher(e,t,n){return r=>dispatchDynamicModelEvent({abortSignal:n,ctx:r.ctx,dynamicModel:t,event:r.event,messages:r.messages,scope:e})}function createNodeHarnessTools(e){let t=new Map;for(let n of e.node.turnAgent.tools){let r=resolveHarnessToolDefinition({node:e.node,tool:n});r!==null&&t.set(n.name,r)}return t}function resolveHarnessToolDefinition(e){let t=findRegisteredRuntimeTool(e.node.toolRegistry,e.tool.name);return isPreparedRuntimeWorkflowTool(e.tool)?t===null?createPreparedWorkflowToolHarnessDefinition(e.tool):createWorkflowToolHarnessDefinition({executeInput:t.definition.executeInput,definition:createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly}),nodeId:e.tool.task.nodeId,workflowId:e.tool.task.workflowId}):t===null?(log.warn(`declared tool is not registered — omitting from toolset`,{toolName:e.tool.name,nodeId:e.node.nodeId}),null):createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly})}function isPreparedRuntimeWorkflowTool(e){return e.task!==void 0}function createRegisteredHarnessToolDefinition(e){let t=e.definition;if(t.owner.kind===`framework`){let n=createTaskToolHarnessDefinitions().find(e=>e.name===t.name);if(n!==void 0)return{...n,behavior:e.behavior}}let n=t.execute;return{availableInSubagents:t.availableInSubagents,label:t.label??(t.owner.kind===`framework`&&t.name===WEB_SEARCH_TOOL_NAME?{start:resolveWebSearchActivityLabel}:void 0),approvalKey:t.approvalKey,behavior:e.behavior,description:t.description,execution:t.execution,executeInput:t.executeInput,execute:resolveAuthoredExecute({rawExecute:n,scope:t.name}),frameworkAction:t.owner.kind===`framework`&&t.name===LOAD_SKILL_TOOL_NAME?`load-skill`:void 0,inputSchema:t.inputSchema??UNSPECIFIED_INPUT_SCHEMA,name:t.name,approval:t.approval,outputSchema:t.outputSchema,rootOnly:e.rootOnly,toModelOutput:t.toModelOutput}}function resolveAuthoredExecute(e){let{rawExecute:t,scope:n}=e;return t===void 0?void 0:createToolExecuteWithAuth({execute:t,scope:n})}export{buildRuntimeIdentity,createExecutionNodeStep,createNodeHarnessTools}; \ No newline at end of file -+import{createLogger}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{UNSPECIFIED_INPUT_SCHEMA}from"#tools/schema.js";import{createToolExecuteWithAuth}from"#execution/tool-auth.js";import{resolveRuntimeModelReference}from"#runtime/agent/resolve-model.js";import{createPreparedWorkflowToolHarnessDefinition,createWorkflowToolHarnessDefinition}from"#execution/tools/workflow/background.js";import{LOAD_SKILL_TOOL_NAME}from"#runtime/skills/fragment-context.js";import{dispatchDynamicModelEvent}from"#context/dynamic-model-lifecycle.js";import{preparePersistedStepDynamicToolMetadata}from"#context/dynamic-tool-lifecycle.js";import{createToolLoopHarness}from"#harness/tool-loop.js";import{createTaskToolHarnessDefinitions}from"#execution/tools/tasks.js";import{findRegisteredRuntimeTool}from"#runtime/tools/registry.js";import{connectionToolReplayIdentity}from"#execution/tools/connection-search.js";import{WEB_SEARCH_TOOL_NAME,resolveWebSearchActivityLabel}from"#harness/provider-tool-schemas.js";const log=createLogger(`execution.node-step`);function createExecutionNodeStep(e){let t=createRuntimeModelResolver(e.modelResolutionScope),n=e.node.turnAgent.dynamicModel===void 0?void 0:createRuntimeDynamicModelEventDispatcher(e.modelResolutionScope,e.node.turnAgent.dynamicModel,e.abortSignal),r=createNodeHarnessTools({node:e.node}),i=e.instrumentation,a=i?.prepareExecution(),o=createToolLoopHarness({steeringSignal:e.steeringSignal,abortSignal:e.abortSignal,capabilities:e.capabilities,clearOnly:e.clearOnly,compactOnly:e.compactOnly,handleEvent:e.handleEvent,historyProjector:e.historyProjector,historyView:e.historyView,instrumentation:a,prepareApprovalTurn:e.prepareApprovalTurn,toolReplayIdentity:connectionToolReplayIdentity,resolveStepDynamicTools:t=>preparePersistedStepDynamicToolMetadata({...t,resolvers:e.node.agent.dynamicToolResolvers??[]}),dispatchDynamicModelEvent:n,resolveModel:t,runtimeIdentity:buildRuntimeIdentity(e.node),tools:r});return i===void 0?o:async(e,t)=>{try{return await o(e,t)}finally{await i.flush()}}}function buildRuntimeIdentity(e){let t=resolveInstalledPackageInfo(),n={agentId:e.turnAgent.id,agentName:e.agent.config?.name,eveVersion:t.version},r=process.env.VERCEL_GIT_COMMIT_SHA?.trim(),i=process.env.VERCEL_GIT_COMMIT_REF?.trim(),a=process.env.VERCEL_DEPLOYMENT_CREATED_AT?.trim();return r||i||a?{...n,build:{deployedAt:a||void 0,gitBranch:i||void 0,gitSha:r||void 0}}:n}function createRuntimeModelResolver(e){return t=>resolveRuntimeModelReference(t,e)}function createRuntimeDynamicModelEventDispatcher(e,t,n){return r=>dispatchDynamicModelEvent({abortSignal:n,ctx:r.ctx,dynamicModel:t,event:r.event,messages:r.messages,scope:e})}function createNodeHarnessTools(e){let t=new Map;for(let n of e.node.turnAgent.tools){let r=resolveHarnessToolDefinition({node:e.node,tool:n});r!==null&&t.set(n.name,r)}return t}function resolveHarnessToolDefinition(e){let t=findRegisteredRuntimeTool(e.node.toolRegistry,e.tool.name);return isPreparedRuntimeWorkflowTool(e.tool)?t===null?createPreparedWorkflowToolHarnessDefinition(e.tool):createWorkflowToolHarnessDefinition({executeInput:t.definition.executeInput,definition:createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly}),nodeId:e.tool.task.nodeId,workflowId:e.tool.task.workflowId}):t===null?(log.warn(`declared tool is not registered — omitting from toolset`,{toolName:e.tool.name,nodeId:e.node.nodeId}),null):createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly})}function isPreparedRuntimeWorkflowTool(e){return e.task!==void 0}function createRegisteredHarnessToolDefinition(e){let t=e.definition;if(t.owner.kind===`framework`||t.name===`task_cancel`){let n=createTaskToolHarnessDefinitions().find(e=>e.name===t.name);if(n!==void 0)return{...n,behavior:e.behavior}}let n=t.execute;return{availableInSubagents:t.availableInSubagents,label:t.label??(t.owner.kind===`framework`&&t.name===WEB_SEARCH_TOOL_NAME?{start:resolveWebSearchActivityLabel}:void 0),approvalKey:t.approvalKey,behavior:e.behavior,description:t.description,execution:t.execution,executeInput:t.executeInput,execute:resolveAuthoredExecute({rawExecute:n,scope:t.name}),frameworkAction:t.owner.kind===`framework`&&t.name===LOAD_SKILL_TOOL_NAME?`load-skill`:void 0,inputSchema:t.inputSchema??UNSPECIFIED_INPUT_SCHEMA,name:t.name,approval:t.approval,outputSchema:t.outputSchema,rootOnly:e.rootOnly,toModelOutput:t.toModelOutput}}function resolveAuthoredExecute(e){let{rawExecute:t,scope:n}=e;return t===void 0?void 0:createToolExecuteWithAuth({execute:t,scope:n})}export{buildRuntimeIdentity,createExecutionNodeStep,createNodeHarnessTools}; ++import{createLogger}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{UNSPECIFIED_INPUT_SCHEMA}from"#tools/schema.js";import{createToolExecuteWithAuth}from"#execution/tool-auth.js";import{resolveRuntimeModelReference}from"#runtime/agent/resolve-model.js";import{createPreparedWorkflowToolHarnessDefinition,createWorkflowToolHarnessDefinition}from"#execution/tools/workflow/background.js";import{LOAD_SKILL_TOOL_NAME}from"#runtime/skills/fragment-context.js";import{dispatchDynamicModelEvent}from"#context/dynamic-model-lifecycle.js";import{preparePersistedStepDynamicToolMetadata}from"#context/dynamic-tool-lifecycle.js";import{createToolLoopHarness}from"#harness/tool-loop.js";import{createTaskToolHarnessDefinitions}from"#execution/tools/tasks.js";import{findRegisteredRuntimeTool}from"#runtime/tools/registry.js";import{WEB_SEARCH_TOOL_NAME,resolveWebSearchActivityLabel}from"#harness/provider-tool-schemas.js";const log=createLogger(`execution.node-step`);function createExecutionNodeStep(e){let t=createRuntimeModelResolver(e.modelResolutionScope),n=e.node.turnAgent.dynamicModel===void 0?void 0:createRuntimeDynamicModelEventDispatcher(e.modelResolutionScope,e.node.turnAgent.dynamicModel,e.abortSignal),r=createNodeHarnessTools({node:e.node}),i=e.instrumentation,a=i?.prepareExecution(),o=createToolLoopHarness({steeringSignal:e.steeringSignal,abortSignal:e.abortSignal,capabilities:e.capabilities,clearOnly:e.clearOnly,compactOnly:e.compactOnly,handleEvent:e.handleEvent,historyProjector:e.historyProjector,historyView:e.historyView,instrumentation:a,mode:e.mode,resolveStepDynamicTools:t=>preparePersistedStepDynamicToolMetadata({...t,resolvers:e.node.agent.dynamicToolResolvers??[]}),dispatchDynamicModelEvent:n,resolveModel:t,runtimeIdentity:buildRuntimeIdentity(e.node),tools:r});return i===void 0?o:async(e,t)=>{try{return await o(e,t)}finally{await i.flush()}}}function buildRuntimeIdentity(e){let t=resolveInstalledPackageInfo(),n={agentId:e.turnAgent.id,agentName:e.agent.config?.name,eveVersion:t.version},r=process.env.VERCEL_GIT_COMMIT_SHA?.trim(),i=process.env.VERCEL_GIT_COMMIT_REF?.trim(),a=process.env.VERCEL_DEPLOYMENT_CREATED_AT?.trim();return r||i||a?{...n,build:{deployedAt:a||void 0,gitBranch:i||void 0,gitSha:r||void 0}}:n}function createRuntimeModelResolver(e){return t=>resolveRuntimeModelReference(t,e)}function createRuntimeDynamicModelEventDispatcher(e,t,n){return r=>dispatchDynamicModelEvent({abortSignal:n,ctx:r.ctx,dynamicModel:t,event:r.event,messages:r.messages,scope:e})}function createNodeHarnessTools(e){let t=new Map;for(let n of e.node.turnAgent.tools){let r=resolveHarnessToolDefinition({node:e.node,tool:n});r!==null&&t.set(n.name,r)}return t}function resolveHarnessToolDefinition(e){let t=findRegisteredRuntimeTool(e.node.toolRegistry,e.tool.name);return isPreparedRuntimeWorkflowTool(e.tool)?t===null?createPreparedWorkflowToolHarnessDefinition(e.tool):createWorkflowToolHarnessDefinition({executeInput:t.definition.executeInput,definition:createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly}),nodeId:e.tool.task.nodeId,workflowId:e.tool.task.workflowId}):t===null?(log.warn(`declared tool is not registered — omitting from toolset`,{toolName:e.tool.name,nodeId:e.node.nodeId}),null):createRegisteredHarnessToolDefinition({behavior:e.tool.behavior,definition:t.definition,rootOnly:e.tool.rootOnly})}function isPreparedRuntimeWorkflowTool(e){return e.task!==void 0}function createRegisteredHarnessToolDefinition(e){let t=e.definition;if(t.owner.kind===`framework`||t.name===`task_cancel`){let n=createTaskToolHarnessDefinitions().find(e=>e.name===t.name);if(n!==void 0)return{...n,behavior:e.behavior}}let n=t.execute;return{availableInSubagents:t.availableInSubagents,label:t.label??(t.owner.kind===`framework`&&t.name===WEB_SEARCH_TOOL_NAME?{start:resolveWebSearchActivityLabel}:void 0),approvalKey:t.approvalKey,behavior:e.behavior,description:t.description,execution:t.execution,executeInput:t.executeInput,execute:resolveAuthoredExecute({rawExecute:n,scope:t.name}),frameworkAction:t.owner.kind===`framework`&&t.name===LOAD_SKILL_TOOL_NAME?`load-skill`:void 0,inputSchema:t.inputSchema??UNSPECIFIED_INPUT_SCHEMA,name:t.name,approval:t.approval,outputSchema:t.outputSchema,rootOnly:e.rootOnly,toModelOutput:t.toModelOutput}}function resolveAuthoredExecute(e){let{rawExecute:t,scope:n}=e;return t===void 0?void 0:createToolExecuteWithAuth({execute:t,scope:n})}export{buildRuntimeIdentity,createExecutionNodeStep,createNodeHarnessTools}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b037d5..75eadfbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: '@linqapp/chat-sdk-adapter@0.5.1': 78b56cdd08c4250ad2680c094858b28d6d6935538d8e59e6dd578f106c77b9e7 - eve@0.67.2: 43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb + eve@0.66.3: b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027 importers: @@ -15,6 +15,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 +51,15 @@ 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.4 + version: 0.2.4(eve@0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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))) + '@stripe/link-sdk': + specifier: 0.11.0 + version: 0.11.0 '@t3-oss/env-nextjs': specifier: 0.13.11 version: 0.13.11(typescript@6.0.3)(zod@4.5.4) @@ -71,7 +83,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.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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 +91,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 @@ -97,11 +109,11 @@ importers: specifier: ^0.45.2 version: 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) eve: - specifier: 0.67.2 - version: 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)) + specifier: 0.66.3 + version: 0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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)) evlog: specifier: 2.28.1 - version: 2.28.1(ai@7.0.120(zod@4.5.4))(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)))(express@5.2.1)(hono@4.13.5)(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))(ofetch@1.5.1)(react@19.2.8)(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)) + version: 2.28.1(ai@7.0.120(zod@4.5.4))(eve@0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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)))(express@5.2.1)(hono@4.13.5)(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))(ofetch@1.5.1)(react@19.2.8)(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)) lucide-react: specifier: 1.34.0 version: 1.34.0(react@19.2.8) @@ -509,63 +521,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 +584,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 +600,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 +2901,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.4': + resolution: {integrity: sha512-vC/Zx1dEmVUfqskjrVITJrxEZaK7iUXXW4/Z+mSYpsNbZyE9hQdUiORbDM/5qRMnb2Ld5tlf6qEJ/RPAP10RiA==} + engines: {node: '>=24'} + peerDependencies: + eve: '*' + + '@stripe/link-sdk@0.11.0': + resolution: {integrity: sha512-23XLYA9Q77519n8RsrGec7OmpWHttTYo18kTAC5kv5NKeMw8tieg6Owdpu/gdNUPWp1d97TETBIZSPXGt90kFg==} + engines: {node: '>=20'} + '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} @@ -3644,15 +3673,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 +3692,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 +3705,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -4523,8 +4549,8 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eve@0.67.2: - resolution: {integrity: sha512-SMdO0GbKLDIN0vsV6zbKca0OlWYaGb79yrdJLIAm7HWWjO/X2M598dJdxVTBWV+20SDtrLv0yrSLmb9CxP5QCQ==} + eve@0.66.3: + resolution: {integrity: sha512-oJkQdVrlmb1GFeaSUNY6IF71ttXjQdFnkKQP+84Qlvs/M+fUqMjGTqnwevQ+qFvc8KCswo3cbBohKOCGdgZPFg==} engines: {node: '>=24'} hasBin: true peerDependencies: @@ -7746,10 +7772,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 +7786,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 +7831,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 +9426,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.4(eve@0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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.11.0 + eve: 0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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.11.0': + dependencies: + zod: 4.5.4 + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -9802,13 +9846,13 @@ 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.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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))) - 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)) + 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.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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)': dependencies: @@ -10293,17 +10337,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 +10365,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): @@ -11110,7 +11153,7 @@ snapshots: etag@1.8.1: {} - 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)): + eve@0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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: ai: 7.0.120(zod@4.5.4) nitro: 3.0.260903-beta @@ -11133,10 +11176,10 @@ snapshots: dependencies: eventsource-parser: 3.1.1 - evlog@2.28.1(ai@7.0.120(zod@4.5.4))(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)))(express@5.2.1)(hono@4.13.5)(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))(ofetch@1.5.1)(react@19.2.8)(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)): + evlog@2.28.1(ai@7.0.120(zod@4.5.4))(eve@0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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)))(express@5.2.1)(hono@4.13.5)(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))(ofetch@1.5.1)(react@19.2.8)(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)): optionalDependencies: ai: 7.0.120(zod@4.5.4) - 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)) + eve: 0.66.3(patch_hash=b862c976047aef5a3b1a514c2eaf1ae1820d93d46792c0081e9a33f58cd02027)(@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)) express: 5.2.1 hono: 4.13.5 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) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7db98eb..01c3bcf0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,10 @@ minimumReleaseAgeStrict: true minimumReleaseAge: 1440 minimumReleaseAgeExclude: + # Explicitly requested Link upgrade; remove once these releases clear the + # 24-hour gate (2026-10-03 15:14 UTC). Future versions remain age-gated. + - "@stripe/link-integrations-eve@0.2.4" + - "@stripe/link-sdk@0.11.0" # Eve 0.44.4's current official scaffold resolves the matching AI SDK release cohort. - "@ai-sdk/*" - ai @@ -22,4 +26,4 @@ allowBuilds: sharp: true patchedDependencies: "@linqapp/chat-sdk-adapter@0.5.1": patches/@linqapp__chat-sdk-adapter@0.5.1.patch - eve@0.67.2: patches/eve@0.67.2.patch + eve@0.66.3: patches/eve@0.66.3.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/agent-tool-boundaries.test.ts b/tests/agent-tool-boundaries.test.ts index 929cd78e..3f0d86c3 100644 --- a/tests/agent-tool-boundaries.test.ts +++ b/tests/agent-tool-boundaries.test.ts @@ -25,6 +25,7 @@ describe("root and worker capability boundaries", () => { "calendar.ts", "contacts.ts", "gmail.ts", + "link__retrieve_spend_request.ts", "messaging.ts", "run_browser.ts", "schedules.ts", @@ -63,6 +64,7 @@ describe("root and worker capability boundaries", () => { "bash.ts", "capture_browser_image.ts", "computer_action.ts", + "fill_from_link.ts", "fill_from_vault.ts", "list_vault.ts", "load_skill.ts", diff --git a/tests/agent/channels/channel-compatibility.test.ts b/tests/agent/channels/channel-compatibility.test.ts index a549cd9d..ded9a7eb 100644 --- a/tests/agent/channels/channel-compatibility.test.ts +++ b/tests/agent/channels/channel-compatibility.test.ts @@ -31,6 +31,7 @@ describe("compiled channel compatibility", () => { const input = { channel: { kind: "http" }, environment: "production", + mode: "conversation", caller: { type: "principal", principal: { diff --git a/tests/agent/subagents/browser-agent/tools/fill-from-link.test.ts b/tests/agent/subagents/browser-agent/tools/fill-from-link.test.ts new file mode 100644 index 00000000..b7824e26 --- /dev/null +++ b/tests/agent/subagents/browser-agent/tools/fill-from-link.test.ts @@ -0,0 +1,239 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { SpendRequest } from "@stripe/link-sdk"; +import * as WorkerAccess from "@agent/subagents/browser-agent/lib/access"; +import * as OwnedBrowser from "@agent/subagents/browser-agent/lib/owned-browser"; +import * as Autofill from "@agent/subagents/browser-agent/lib/autofill/native"; +import fillFromLink from "@agent/subagents/browser-agent/tools/fill_from_link"; +import { linkAuth } from "@agent/lib/link-auth"; +import { toolContextFor } from "@tests/helpers/tool-context"; + +const fetchMock = vi.fn(); +const scope = { userId: "user-1", workspaceId: "workspace-1" }; +const input = { + browserSessionId: "browser-1", + spendRequestId: "spr_123", + amount: 2306, + currency: "usd", +}; +const context = toolContextFor({ parentSessionId: "root-1" }); +const access = vi.spyOn(WorkerAccess, "requireWorkerScope"); +const owned = vi.spyOn(OwnedBrowser, "requireOwnedBrowserSession"); +const origin = vi.spyOn(Autofill, "currentKernelPageOrigin"); +const fill = vi.spyOn(Autofill, "fillWithKernelNativeAutofill"); +const token = vi.spyOn(context, "getToken"); +const requireAuth = vi.spyOn(context, "requireAuth"); + +function approvedRequest(): SpendRequest { + return { + id: input.spendRequestId, + status: "approved", + amount: 2306, + currency: "usd", + credential_type: "card", + merchant_url: "https://shop.example/checkout", + created_at: "2026-10-01T00:00:00Z", + updated_at: "2026-10-01T00:00:00Z", + expires_at: 2082758400, + card: { + id: "card_123", + brand: "visa", + number: "4242424242424242", + cvc: "098", + exp_month: 12, + exp_year: 2035, + valid_until: "2035-12-01T00:00:00Z", + billing_address: { + name: "Test Buyer", + line1: "1 Test St", + country: "US", + }, + }, + }; +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + access.mockResolvedValue(scope); + owned.mockResolvedValue({ + createdAt: "2026-10-01T00:00:00Z", + sessionId: "browser-1", + workerSessionId: "worker-1", + }); + token.mockResolvedValue({ token: "wallet-token-user-1" }); + origin.mockResolvedValue("https://shop.example"); + fill.mockResolvedValue({ filledClaims: 5, origin: "https://shop.example" }); + fetchMock.mockImplementation(async () => Response.json(approvedRequest())); +}); +afterEach(() => vi.unstubAllGlobals()); + +describe("Link browser bridge", () => { + it("uses the real SDK to retrieve a scoped card and sends secrets only to the injector", async () => { + const output = await fillFromLink.execute(input, context); + expect(token).toHaveBeenCalledWith(linkAuth); + expect(owned).toHaveBeenCalledWith(scope, input.browserSessionId); + const call = fetchMock.mock.calls[0]; + if (!call) throw new Error("Missing SDK request"); + const [resource, options] = call; + expect(resource).toEqual( + expect.stringContaining("/spend_requests/spr_123?include=card") + ); + expect(new Headers(options?.headers).get("Authorization")).toBe( + "Bearer wallet-token-user-1" + ); + expect(options).toMatchObject({ + method: "GET", + redirect: "error", + cache: "no-store", + }); + const args = fill.mock.calls[0]?.[0]; + if (!args) throw new Error("Missing injector call"); + expect(Autofill.buildNativeAutofillPayload("payment", args.claims)).toEqual( + { + card: { + name: "Test Buyer", + number: "4242424242424242", + cvc: "098", + expiryMonth: "12", + expiryYear: "2035", + }, + } + ); + expect(args.expectedOrigin).toBe("https://shop.example"); + expect(output).toEqual({ + success: true, + spendRequestId: "spr_123", + origin: "https://shop.example", + amount: 2306, + currency: "usd", + filledClaims: 5, + }); + expect(JSON.stringify(output)).not.toMatch( + /4242424242424242|098|wallet-token|Test Buyer/u + ); + }); + + it.each([ + "pending_approval", + "requires_action", + "denied", + "expired", + "submitted", + "succeeded", + "canceled", + "future_status", + ])("rejects %s requests", async (status) => { + fetchMock.mockResolvedValue( + Response.json({ ...approvedRequest(), status }) + ); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + "not currently approved" + ); + expect(fill).not.toHaveBeenCalled(); + }); + + it.each([ + { id: "other_request" }, + { amount: 1 }, + { currency: "eur" }, + { merchant_url: "http://shop.example/checkout" }, + { merchant_url: "https://attacker.example" }, + { merchant_url: "https://user:pass@shop.example" }, + { merchant_url: undefined }, + { credential_type: "shared_payment_token" }, + { shared_payment_token: { id: "spt_secret" } }, + { link_pay_token: "lpt_secret" }, + { recurring: { interval: "month", interval_count: 1 } }, + { expires_at: 1 }, + { card: undefined }, + ])( + "rejects mismatched, expired, or unsupported requests: %j", + async (change) => { + fetchMock.mockResolvedValue( + Response.json({ ...approvedRequest(), ...change }) + ); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + /Link|checkout/u + ); + expect(fill).not.toHaveBeenCalled(); + } + ); + + it.each([ + { cvc: undefined }, + { number: "invalid" }, + { exp_year: 2020 }, + { valid_until: "invalid" }, + { valid_until: "1" }, + { billing_address: undefined }, + ])("rejects unusable card data: %j", async (change) => { + const request = approvedRequest(); + fetchMock.mockResolvedValue( + Response.json({ ...request, card: { ...request.card, ...change } }) + ); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + /Link|checkout/u + ); + expect(fill).not.toHaveBeenCalled(); + }); + + it("handles a request absent from this wallet", async () => { + fetchMock.mockResolvedValue(new Response(null, { status: 404 })); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + "not currently approved" + ); + expect(fill).not.toHaveBeenCalled(); + }); + + it.each([access, owned])( + "rejects unowned sessions before wallet access", + async (check) => { + check.mockRejectedValueOnce(new Error("not owned")); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + "not owned" + ); + expect(token).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(fill).not.toHaveBeenCalled(); + } + ); + + it("preserves Eve authorization suspension", async () => { + const challenge = new Error("connect-wallet"); + token.mockRejectedValueOnce(challenge); + await expect(fillFromLink.execute(input, context)).rejects.toBe(challenge); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("requests reauthorization on 401 without exposing the response", async () => { + fetchMock.mockResolvedValue( + Response.json({ error: "secret-response" }, { status: 401 }) + ); + requireAuth.mockImplementationOnce(() => { + throw new Error("reauthorize"); + }); + await expect(fillFromLink.execute(input, context)).rejects.toThrow( + "reauthorize" + ); + expect(requireAuth).toHaveBeenCalledWith(linkAuth); + expect(fill).not.toHaveBeenCalled(); + }); + + it("redacts SDK error bodies and causes", async () => { + fetchMock.mockResolvedValue( + Response.json({ error: "4242424242424242" }, { status: 500 }) + ); + const result = fillFromLink.execute(input, context); + await expect(result).rejects.toThrow("Could not retrieve"); + await expect(result).rejects.not.toHaveProperty("cause"); + expect(fill).not.toHaveBeenCalled(); + }); + + it("does not return or retry an ambiguous injector error", async () => { + fill.mockRejectedValueOnce(new Error("4242424242424242")); + const result = fillFromLink.execute(input, context); + await expect(result).rejects.toThrow("could not be confirmed"); + await expect(result).rejects.not.toHaveProperty("cause"); + expect(fill).toHaveBeenCalledOnce(); + }); +}); diff --git a/tests/agent/tools/link-retrieve-spend-request.test.ts b/tests/agent/tools/link-retrieve-spend-request.test.ts new file mode 100644 index 00000000..14d2f097 --- /dev/null +++ b/tests/agent/tools/link-retrieve-spend-request.test.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { retrieve_spend_request } from "@stripe/link-integrations-eve/tools"; +import retrieve from "@agent/tools/link__retrieve_spend_request"; +import { toolContextFor } from "@tests/helpers/tool-context"; + +const execute = vi.spyOn(retrieve_spend_request, "execute"); +beforeEach(() => vi.clearAllMocks()); + +describe("Link status-only override", () => { + it("delegates to the official extension without requesting credentials and projects safe fields", async () => { + execute.mockResolvedValue({ + id: "spr_1", + status: "approved", + created_at: "2026-10-01", + updated_at: "2026-10-01", + amount: 2306, + currency: "usd", + merchant_url: "https://shop.example", + approval_url: "https://link.com/approve", + card: { + id: "card_1", + brand: "visa", + number: "4242424242424242", + cvc: "098", + exp_month: 12, + exp_year: 2035, + }, + link_pay_token: "lpt_secret", + shared_payment_token: { id: "spt_secret" }, + }); + const context = toolContextFor(); + const output = await retrieve.execute({ id: "spr_1" }, context); + expect(execute).toHaveBeenCalledExactlyOnceWith({ id: "spr_1" }, context); + expect(output).toMatchObject({ + id: "spr_1", + status: "approved", + amount: 2306, + currency: "usd", + }); + expect(JSON.stringify(output)).not.toMatch( + /4242424242424242|098|lpt_secret|spt_secret/u + ); + if (!(retrieve.inputSchema instanceof z.ZodType)) + throw new Error("Expected Zod schema"); + expect( + retrieve.inputSchema.safeParse({ id: "spr_1", include: ["card"] }).success + ).toBe(false); + }); + + it("returns null for a missing request", async () => { + execute.mockResolvedValue(null); + await expect( + retrieve.execute({ id: "missing" }, toolContextFor()) + ).resolves.toBeNull(); + }); +}); diff --git a/tests/integration/link.test.ts b/tests/integration/link.test.ts new file mode 100644 index 00000000..0f9d80f1 --- /dev/null +++ b/tests/integration/link.test.ts @@ -0,0 +1,312 @@ +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", () => { + // 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"); + 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(); + } + }, 15_000); +}); + +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_*"