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..9bbca9ac 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,53 @@ 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 upgrades + +This application uses Eve `0.69.0`. Browser work runs as a resumable task: +`run_browser` returns a task receipt, subsequent calls use `taskId`, and +`task_cancel` stops the task's current work while keeping its browser session +available for another assignment. Task results arrive within the open turn. + +Eve rejects session handoffs across the 0.69 upgrade boundary. Keep an existing +session on its owning deployment until it finishes, and start a new conversation +on the upgraded deployment. This migration does not cancel or reset existing +production sessions. Update remote-agent deployments before their callers when +using remote agents. + ## Local development The **Deploy with Vercel** flow above is the simplest way to run OpenInstinct. It diff --git a/agent/channels/eve.ts b/agent/channels/eve.ts index aa0c5581..73d4e001 100644 --- a/agent/channels/eve.ts +++ b/agent/channels/eve.ts @@ -71,6 +71,14 @@ const channel = eveChannel({ auth: authenticate, events: { async "action.result"(event, _channel, session) { + if ( + event.status === "completed" && + event.result.kind === "tool-result" && + event.result.toolName === "no_reply" + ) { + await finalizeScheduledReportDelivery(session, "suppressed"); + return; + } if ( event.status === "completed" && sendMessageToolResultSchema.safeParse(event.result).success diff --git a/agent/channels/linq.ts b/agent/channels/linq.ts index 37c0e296..d6a4bc60 100644 --- a/agent/channels/linq.ts +++ b/agent/channels/linq.ts @@ -68,6 +68,14 @@ export default linqChannel({ credentials, events: { async "action.result"(event, context, session) { + if ( + event.status === "completed" && + event.result.kind === "tool-result" && + event.result.toolName === "no_reply" + ) { + await finalizeScheduledReportDelivery(session, "suppressed"); + return; + } const reaction = reactToMessageToolResultSchema.safeParse(event.result); if (event.status === "completed" && reaction.success) { if (!context.thread) { diff --git a/agent/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/hooks/background-reply-target.ts b/agent/hooks/background-reply-target.ts index f672f6da..5c4ef9e0 100644 --- a/agent/hooks/background-reply-target.ts +++ b/agent/hooks/background-reply-target.ts @@ -1,17 +1,11 @@ import { defineHook } from "eve/hooks"; import { registerBackgroundReplyTarget } from "@agent/lib/reply-targets"; -import { browserTaskReceiptSchema } from "@agent/lib/browser-task"; export default defineHook({ events: { - "action.result"(event, context) { - if (event.data.status !== "completed") return; - const task = browserTaskReceiptSchema.safeParse(event.data.result); - if (!task.success) return; - registerBackgroundReplyTarget( - task.data.output.taskId, - context.session.auth - ); + "task.started"(event, context) { + if (event.data.name !== "run_browser") return; + registerBackgroundReplyTarget(event.data.taskId, context.session.auth); }, }, }); diff --git a/agent/hooks/scheduled-run-completion.ts b/agent/hooks/scheduled-run-completion.ts index 05ba62f8..69a65398 100644 --- a/agent/hooks/scheduled-run-completion.ts +++ b/agent/hooks/scheduled-run-completion.ts @@ -1,10 +1,8 @@ import { defineHook } from "eve/hooks"; -import { browserTaskReceiptSchema } from "@agent/lib/browser-task"; import { scheduledRunIdentity } from "@agent/lib/schedules/identity"; import { scheduledRunOutcomeSchema } from "@shared/schedules/outcome"; import { completeScheduledAgentRun, - deferScheduledAgentRunCompletion, markScheduledAgentRunStarted, releaseScheduledAgentRun, waitForScheduledAgentRunInput, @@ -62,26 +60,6 @@ export default defineHook({ sessionId: ctx.session.id, }); }, - async "action.result"(event, ctx) { - if (event.data.status !== "completed") return; - const task = browserTaskReceiptSchema.safeParse(event.data.result); - if (!task.success) return; - const identity = scheduledRunIdentity(ctx.session.auth); - if (!identity) return; - const deferred = await deferScheduledAgentRunCompletion( - identity.runId, - identity.leaseToken, - ctx.session.turn.id, - new Date(event.meta.at) - ); - console.info("[scheduled-run] worker delegated background work", { - deferred, - runId: identity.runId, - sessionId: ctx.session.id, - taskId: task.data.output.taskId, - turnId: ctx.session.turn.id, - }); - }, async "message.completed"(event, ctx) { const identity = scheduledRunIdentity(ctx.session.auth); if (!identity) return; @@ -102,7 +80,7 @@ export default defineHook({ logDeadLetterReportQueued(status, identity.runId, ctx.session.id); return; } - const message = event.data.message?.trim().slice(0, 4_000); + const message = event.data.message.trim().slice(0, 4_000); const outcome = scheduledRunOutcomeSchema.parse( message ? { diff --git a/agent/instructions/content/role/interactive.md b/agent/instructions/content/role/interactive.md index 9fc676ad..b91066ee 100644 --- a/agent/instructions/content/role/interactive.md +++ b/agent/instructions/content/role/interactive.md @@ -56,15 +56,15 @@ The main conversation is the control plane. Coordinate the user's work there and - Eve and Linq own read receipts, typing indicators, delivery state, authorization prompts, and approval/input cards. Let those native control-plane features operate normally; do not duplicate them as prose unless the user needs an explanation. - After a `send_message` or `react_to_message` call, never repeat or summarize it in assistant text. If the runtime requires terminal assistant text after the last delivery, emit only `DELIVERY_COMPLETE`; ordinary assistant text is not delivered to Linq. - The worker's structured result is coordinator-facing only. Rewrite it into a concise user-facing response; never imply that the worker spoke to the user. -- Start a background worker without a separate preamble. Once its working receipt arrives, send one short acknowledgment saying what is underway. Treat the receipt as acceptance, not completion. -- Keep intermediate background-task wakes silent unless the user must act. When the worker settles, compose the result according to the iMessage composition rules, including a separate follow-up message when the user has a distinct choice or approval to make. -- End the turn whenever the root conversation genuinely needs an answer. Before surfacing a `Needs user input:` blocker for routine contact, traveler, or address fields, confirm the worker explicitly reported checking compatible vault items. If it did not, continue the same worker once and instruct it to call `list_vault` and `fill_from_vault`; only surface the question if no compatible item exists or secure fill still fails. After the user replies, continue that worker with its `agentId` and the supplied answer so it retains its browser state and context. If the answer is an OTP, pass it immediately without echoing it in assistant text. +- Start a browser task without a separate preamble. Once its working receipt arrives, send one short acknowledgment saying what is underway. Treat the receipt as acceptance, not completion. +- Tasks report results within the open turn. Keep intermediate updates silent unless the user must act. When the worker settles, compose the result according to the iMessage composition rules, including a separate follow-up message when the user has a distinct choice or approval to make. +- End the turn whenever the root conversation genuinely needs an answer. Before surfacing a `Needs user input:` blocker for routine contact, traveler, or address fields, confirm the worker explicitly reported checking compatible vault items. If it did not, continue the same worker once and instruct it to call `list_vault` and `fill_from_vault`; only surface the question if no compatible item exists or secure fill still fails. After the user replies, continue that worker with its `taskId` and the supplied answer so it retains its browser state and context. If the answer is an OTP, pass it immediately without echoing it in assistant text. # Worker coordination - Delegate to `browser-agent` only when the task requires interaction with a known site, authenticated or browser-local state, visual inspection, form entry, or content that `web_search` and `web_fetch` could not retrieve. Do not use a generic agent copy or any browser-execution tool yourself. When public discovery is needed before interaction, complete it with `web_search` first and include the useful target URLs and facts in the worker assignment. - Worker images are private, user-scoped artifacts. Ask the worker for and include visual artifacts when they materially help the user verify a completed outcome, compare genuinely visual options, or understand a concrete result. Choose the smallest useful set: one image for a single result, or two to four distinct images when a compact gallery adds real comparison or verification value. Useful cases include showing a purchased item or safe, non-sensitive confirmation state and comparing clothing, food, product, or other visually differentiated options. Put two to four selected artifact references in the same `send_message` call so Linq can present them as one native media gallery; keep its text to one short caption, identify images by order only when the mapping matters, and use a separate short selection question when needed. Do not use a gallery for text-heavy comparisons such as flights, schedules, or prices unless the images materially improve the decision. Skip decorative, repetitive, text-only, or sensitive visuals. Render every selected artifact in order with its exact descriptor as `![label](/artifacts/id)`. Never invent an artifact, change its id or URL, expose a private Blob URL, or claim an internal screenshot was delivered. - When a visually selected product is unavailable, treat images as part of the replacement outcome even if the user does not ask for them again. Find the strongest viable alternatives, ask the worker for one clear artifact per recommendation, and return a compact two-to-four-image gallery with the concrete availability details. -- When the worker returns a missing vault item, call `request_vault_setup` and give its link to the user before ending the turn; do not surface any live browser URL from that result. For a purchase decision, authentication challenge after vault fill, unresolved CAPTCHA after Kernel's managed solver wait, ambiguous choice, or human-takeover blocker, ask the user in the root conversation. Preserve the worker's `agentId` and live browser URL when available, then continue that same parked worker after the user responds so it re-reads the current page before acting. -- Treat a new user message as current steering. Preserve unrelated work. When it revises an active worker task, cancel its `run_browser` task with `task_cancel`, wait for cancellation to settle, then call `run_browser` with the parked worker's `agentId` and the complete revised assignment. Do not send another workflow call to a worker that is still running. Use `task_cancel` when the user wants work stopped without a replacement assignment. Reuse an available worker only when its prior browser state and context remain useful. Cancellation is cooperative and does not roll back external effects, so do not promise atomic interruption. +- When the worker returns a missing vault item, call `request_vault_setup` and give its link to the user before ending the turn; do not surface any live browser URL from that result. For a purchase decision, authentication challenge after vault fill, unresolved CAPTCHA after Kernel's managed solver wait, ambiguous choice, or human-takeover blocker, ask the user in the root conversation. Preserve the worker's `taskId` and live browser URL when available, then continue that same parked worker after the user responds so it re-reads the current page before acting. +- Treat a new user message as current steering. Preserve unrelated work. When it revises an active worker task, cancel its `run_browser` task with `task_cancel`, wait for cancellation to settle, then call `run_browser` with the parked worker's `taskId` and the complete revised assignment. Do not send another workflow call to a worker that is still running. Use `task_cancel` when the user wants work stopped without a replacement assignment. Reuse an available worker only when its prior browser state and context remain useful. Cancellation is cooperative and does not roll back external effects, so do not promise atomic interruption. - Do not create overlapping workers for the same assignment. Do not delegate non-browser work merely to create activity. diff --git a/agent/instructions/content/role/scheduled-report.md b/agent/instructions/content/role/scheduled-report.md index 1bd3c8c0..30048410 100644 --- a/agent/instructions/content/role/scheduled-report.md +++ b/agent/instructions/content/role/scheduled-report.md @@ -7,6 +7,6 @@ You are OpenInstinct evaluating the completed outcome of a background scheduled - Consider the outcome together with the current conversation and the time for which it was scheduled. - This turn exists only to report the outcome, resume its run with context already present in the conversation, or create a secure setup link for a vault item the run needs. Never invoke another agent, alter a schedule or profile, read or change vault contents, access an account, or perform any other external action. - When the run is waiting for input, answer it with `schedules-answer` if the existing conversation resolves the request clearly, then do not send a message. If it needs a supported vault item, call `request_vault_setup` with only the safe metadata in the request, then call `send_message` exactly once with the returned setup link and ask the user to reply when finished. For any other unresolved request, call `send_message` exactly once with its question. Preserve the internal run ID in context for a later `schedules-answer` call, and never guess or expose it to the user. -- For a completed outcome, call `send_message` exactly once only when it is still useful, actionable, time-sensitive, or materially changes what the user knows. Otherwise finish with exactly `` and do not call `send_message`. +- For a completed outcome, call `send_message` exactly once only when it is still useful, actionable, time-sensitive, or materially changes what the user knows. Otherwise call `no_reply` to end the turn silently. - Rewrite useful information as a natural message from OpenInstinct. Never mention the internal worker, handoff, reporting state, or implementation details. - After `send_message`, emit only `DELIVERY_COMPLETE` if the runtime requires terminal assistant text. Never repeat or summarize the delivered message. diff --git a/agent/instructions/content/worker-coordination.md b/agent/instructions/content/worker-coordination.md index 2558ffa4..f4bed94e 100644 --- a/agent/instructions/content/worker-coordination.md +++ b/agent/instructions/content/worker-coordination.md @@ -2,6 +2,6 @@ - Use `run_browser` to give `browser-agent` one bounded browser outcome, the exact target URL when known, all relevant non-secret context, the user's constraints, and any exact transaction approval already granted. The worker does not see the parent conversation. Preserve the requested visual deliverable in the assignment: treat a photo, pic, or image of a specific webpage item, product, dish, or listing as the item's actual page image, while a screenshot or page view means rendered screen context. When authentication may be required, tell it to return `Needs vault setup:` with safe login metadata if no compatible login exists, without a live-view URL or a request for manual credential entry. Tell it to return `Needs user input:` asking for the code when an OTP blocks progress; do not make live view the OTP path. - Begin every worker `message` with `Task: `, followed by a blank line and the complete assignment. Keep the summary free of credentials and sensitive personal details, and reuse it when continuing the same worker. -- The `run_browser` tool supplies the required structured completion schema on every initial or resumed call, including when passing an existing `agentId`. Supply only the assignment and optional `agentId`; do not supply an `outputSchema`. +- The `run_browser` tool supplies the required structured completion schema on every initial or resumed call, including when passing an existing `taskId`. Supply only the assignment and optional `taskId`; do not supply an `outputSchema`. - The worker must finish each browser assignment by calling Eve's native `final_output` tool exactly once with a result matching the required `outputSchema`, then stop without prose, JSON text, another tool, or a second completion. Treat `success` as achieved only when its message includes a verified outcome. Treat `failure` as a blocker or incomplete outcome, not proof that no progress occurred. - Worker images are private, user-scoped artifacts. Ask for visual artifacts only when they materially help verify an outcome or compare genuinely visual options, and choose the smallest useful set. Preserve each selected artifact's exact descriptor as `![label](/artifacts/id)`; never invent an artifact, change its id or URL, expose a private Blob URL, or claim an internal screenshot was delivered. diff --git a/agent/lib/browser-task.ts b/agent/lib/browser-task.ts deleted file mode 100644 index af0382e3..00000000 --- a/agent/lib/browser-task.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const browserTaskReceiptSchema = z.object({ - kind: z.literal("tool-result"), - toolName: z.literal("run_browser"), - output: z.object({ status: z.literal("working"), taskId: z.string().min(1) }), -}); diff --git a/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts b/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts index e062e7ee..e16f7155 100644 --- a/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts +++ b/agent/lib/google-workspace/tests/google-workspace-generated-clients.test.ts @@ -190,6 +190,7 @@ function toolContext() { async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, + messages: [], abortSignal: new AbortController().signal, callId: "call-1", getToken, diff --git a/agent/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/lib/trace/timeline.ts b/agent/subagents/browser-agent/lib/trace/timeline.ts index 1d60aa54..3eeace52 100644 --- a/agent/subagents/browser-agent/lib/trace/timeline.ts +++ b/agent/subagents/browser-agent/lib/trace/timeline.ts @@ -69,7 +69,7 @@ export function traceTimelineRows(event: HookEvent): TraceTimelineRow[] { ]; } case "message.completed": - return [row("Assistant", event.data.message ?? "")]; + return [row("Assistant", event.data.message)]; case "result.completed": return [row("Final output", compactJson(event.data.result))]; case "input.requested": diff --git a/agent/tools/no_reply.ts b/agent/tools/no_reply.ts new file mode 100644 index 00000000..c420cebf --- /dev/null +++ b/agent/tools/no_reply.ts @@ -0,0 +1,14 @@ +import { defineDynamic } from "eve/tools"; +import { noReply } from "eve/tools/no_reply"; +import { resolveModeValue } from "@agent/lib/mode"; + +const noReplyTool = noReply(); + +export default defineDynamic({ + events: { + "turn.started": (_event, context) => + resolveModeValue(context, { + "scheduled-report": { no_reply: noReplyTool }, + }), + }, +}); diff --git a/agent/tools/run_browser.ts b/agent/tools/run_browser.ts index 2cb01ee7..d85d7a89 100644 --- a/agent/tools/run_browser.ts +++ b/agent/tools/run_browser.ts @@ -4,20 +4,31 @@ import { taskCompletionSchema } from "@agent/subagents/browser-agent/lib/complet export default defineWorkflowTool({ description: - "Execute one bounded browser assignment and return a verified result with optional private image artifacts. Pass agentId to continue a parked worker. The completion schema is supplied automatically.", - inputSchema: z.object({ - message: z.string().min(1), - agentId: z.string().optional(), - }), - execution: "background", - async execute(input, ctx) { + "Execute one bounded browser assignment and return a verified result with optional private image artifacts. Continue the same worker using its taskId. The completion schema is supplied automatically.", + inputSchema: z.object({ message: z.string().min(1) }), + async serve(receive, ctx) { "use workflow"; - const result = await ctx.agent("browser-agent", { - ...input, - outputSchema: z - .record(z.string(), z.json()) - .parse(z.toJSONSchema(taskCompletionSchema)), - }); - return taskCompletionSchema.parse(result); + const worker = ctx.agent("browser-agent"); + /* oxlint-disable eslint/no-await-in-loop -- A resumable task serves sequential calls on one child session. */ + for (;;) { + const { input, abortSignal } = await receive(); + try { + const response = await worker.send(input.message, { + outputSchema: taskCompletionSchema, + signal: abortSignal, + }); + const result = await response.result(); + if (abortSignal.aborted) continue; + if (result.status === "failed") { + throw new Error( + result.error?.message ?? "The browser worker failed." + ); + } + ctx.reply(taskCompletionSchema.parse(result.data)); + } catch (error) { + if (!abortSignal.aborted) throw error; + } + } + /* oxlint-enable eslint/no-await-in-loop */ }, }); diff --git a/agent/tools/task_cancel.ts b/agent/tools/task_cancel.ts deleted file mode 100644 index 66342e81..00000000 --- a/agent/tools/task_cancel.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "eve/tools/task_cancel"; diff --git a/app/(authenticated)/_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)/chat/[sessionId]/_components/activity/card.tsx b/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx index d971e7eb..4ef8b9c0 100644 --- a/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx +++ b/app/(authenticated)/chat/[sessionId]/_components/activity/card.tsx @@ -81,20 +81,19 @@ export function ActivityCard({
{sessions.map((session) => { - const status = - statuses.get(session.childSessionId) ?? "starting"; + const status = statuses.get(session.sessionId) ?? "starting"; const task = getSubagentTask( - eventsBySession.get(session.childSessionId) ?? [] + eventsBySession.get(session.sessionId) ?? [] ) ?? session.task; return ( + + )} + {connected && ( +
+ + +
+ )} +
+ )} + + ); +} diff --git a/app/_lib/subagent-sessions.test.ts b/app/_lib/subagent-sessions.test.ts index 7050d3ce..3660740c 100644 --- a/app/_lib/subagent-sessions.test.ts +++ b/app/_lib/subagent-sessions.test.ts @@ -9,17 +9,14 @@ import { const meta = { at: "2026-08-27T12:00:00.000Z", id: "evt_01" }; const called = { - type: "subagent.called", + type: "agent.started", data: { callId: "call_1", - childSessionId: "child_1", - childStreamPath: "/eve/v1/session/child_1/stream", + sessionId: "child_1", + taskId: "task_1", + streamPath: "/eve/v1/session/child_1/stream", name: "researcher", - sequence: 0, - sessionId: "parent_1", - toolName: "researcher", turnId: "turn_1", - workflowId: "workflow_1", }, meta, } satisfies MessageStreamEvent; @@ -29,16 +26,25 @@ describe("collectSubagentSessions", () => { const events = [ called, { - ...called, - data: { ...called.data, callId: "call_2" }, + type: "task.started", + data: { + callId: "call_2", + taskId: "task_1", + turnId: "turn_2", + name: "run_browser", + kind: "tool", + }, meta: { ...meta, id: "evt_02" }, }, { - type: "subagent.completed", + type: "task.settled", data: { callId: "call_2", output: "Done", - subagentName: "researcher", + name: "researcher", + taskId: "task_1", + turnId: "turn_1", + status: "completed", }, meta: { ...meta, id: "evt_03" }, }, @@ -59,16 +65,14 @@ describe("collectSubagentSessions", () => { data: { ...called.data, callId: "call_2", - childSessionId: "child_2", + sessionId: "child_2", }, meta: { ...meta, id: "evt_02" }, }, ] satisfies readonly MessageStreamEvent[]; expect( - collectSubagentSessions(events).map( - ({ childSessionId }) => childSessionId - ) + collectSubagentSessions(events).map(({ sessionId }) => sessionId) ).toEqual(["child_2", "child_1"]); }); @@ -77,6 +81,7 @@ describe("collectSubagentSessions", () => { { type: "actions.requested", data: { + sequence: 0, actions: [ { callId: "call_1", @@ -88,7 +93,6 @@ describe("collectSubagentSessions", () => { subagentName: "researcher", }, ], - sequence: 0, stepIndex: 0, turnId: "turn_1", }, @@ -106,7 +110,7 @@ describe("collectSubagentSessions", () => { data: { ...called.data, callId: "call_2", - childSessionId: "child_2", + sessionId: "child_2", }, meta: { ...meta, id: "evt_02" }, } satisfies MessageStreamEvent; @@ -118,7 +122,7 @@ describe("collectSubagentSessions", () => { expect( collectSubagentSessions([called, secondChild, continuedFirstChild]).map( - ({ childSessionId }) => childSessionId + ({ sessionId }) => sessionId ) ).toEqual(["child_1", "child_2"]); }); @@ -147,19 +151,7 @@ describe("getSubagentSubscriptionKey", () => { describe("getSubagentStatus", () => { it("uses the child stream instead of a background receipt", () => { - const [session] = collectSubagentSessions([ - called, - { - type: "subagent.completed", - data: { - backgroundTask: { status: "working", taskId: "task_1" }, - callId: "call_1", - output: "Continuing in the background", - subagentName: "researcher", - }, - meta: { ...meta, id: "evt_02" }, - }, - ]); + const [session] = collectSubagentSessions([called]); if (!session) throw new Error("Expected a collected subagent session"); @@ -191,9 +183,9 @@ describe("getSubagentStatus", () => { ? { type, data: { + sequence: 0, code: "failed", message: "Failed", - sequence: 0, turnId: "turn_1", }, meta: { ...meta, id: "evt_02" }, @@ -226,8 +218,8 @@ describe("getSubagentTask", () => { { type: "message.received", data: { - message: "Task: First task\n\nFull assignment", sequence: 0, + message: "Task: First task\n\nFull assignment", turnId: "turn_1", }, meta, diff --git a/app/_lib/subagent-sessions.ts b/app/_lib/subagent-sessions.ts index faa660c7..6a85aaef 100644 --- a/app/_lib/subagent-sessions.ts +++ b/app/_lib/subagent-sessions.ts @@ -1,11 +1,12 @@ import type { MessageStreamEvent, - SubagentCalledStreamEvent, - SubagentCompletedStreamEvent, + AgentStartedStreamEvent, + TaskSettledStreamEvent, + TaskStartedStreamEvent, } from "eve/client"; -export type SubagentSession = SubagentCalledStreamEvent["data"] & { - readonly completion?: SubagentCompletedStreamEvent["data"]; +export type SubagentSession = AgentStartedStreamEvent["data"] & { + readonly completion?: TaskSettledStreamEvent["data"]; readonly task?: string; }; @@ -20,40 +21,47 @@ export type SubagentStatus = export function collectSubagentSessions( events: readonly MessageStreamEvent[] ): readonly SubagentSession[] { - const completions = new Map(); - const tasks = new Map(); + const completions = new Map(); + const calls = new Map(); + const descriptions = new Map(); const sessions = new Map(); - - for (const event of events) { - if (event.type === "subagent.completed") { - completions.set(event.data.callId, event.data); - continue; + const callOrder = new Map(); + for (const [index, event] of events.entries()) { + if (event.type === "task.started" || event.type === "agent.started") { + if (event.type === "task.started" || !callOrder.has(event.data.callId)) + callOrder.set(event.data.callId, index); } + if (event.type === "task.started") calls.set(event.data.taskId, event.data); + if (event.type === "task.settled") + completions.set(event.data.callId, event.data); if (event.type === "actions.requested") { for (const action of event.data.actions) { if ( action.kind === "subagent-call" || action.kind === "remote-agent-call" ) { - tasks.set(action.callId, action.description); + descriptions.set(action.callId, action.description); } } } } - for (const event of events) { - if (event.type !== "subagent.called") continue; - + if (event.type !== "agent.started") continue; + const call = event.data.taskId ? calls.get(event.data.taskId) : undefined; + const callId = call?.callId ?? event.data.callId; const session = { ...event.data, - completion: completions.get(event.data.callId), - task: tasks.get(event.data.callId), + callId, + turnId: call?.turnId ?? event.data.turnId, + completion: completions.get(callId), + task: descriptions.get(callId), }; - sessions.delete(session.childSessionId); - sessions.set(session.childSessionId, session); + sessions.delete(session.sessionId); + sessions.set(session.sessionId, session); } - - return [...sessions.values()].toReversed(); + return [...sessions.values()].toSorted( + (a, b) => (callOrder.get(b.callId) ?? 0) - (callOrder.get(a.callId) ?? 0) + ); } export function getSubagentSubscriptionKey( @@ -62,7 +70,7 @@ export function getSubagentSubscriptionKey( return sessions .map( (session) => - `${encodeURIComponent(session.childSessionId)}:${encodeURIComponent(session.callId)}` + `${encodeURIComponent(session.sessionId)}:${encodeURIComponent(session.callId)}` ) .join("\n"); } @@ -78,8 +86,7 @@ export function getSubagentStatus( ); if (terminalSession?.type === "session.completed") return "complete"; if (terminalSession?.type === "session.failed") return "failed"; - - const latestTurnBoundary = events + const boundary = events .toReversed() .find((event) => [ @@ -89,12 +96,17 @@ export function getSubagentStatus( "turn.started", ].includes(event.type) ); - if (latestTurnBoundary?.type === "turn.failed") return "failed"; - if (latestTurnBoundary?.type === "turn.cancelled") return "cancelled"; - if (latestTurnBoundary?.type === "turn.completed") return "ready"; - if (latestTurnBoundary?.type === "turn.started") return "working"; - if (events.some((event) => event.type === "session.waiting")) return "ready"; - if (session.completion && !session.completion.backgroundTask) return "ready"; + if (boundary?.type === "turn.failed") return "failed"; + if (boundary?.type === "turn.cancelled") return "cancelled"; + if (boundary?.type === "turn.completed") return "ready"; + if (boundary?.type === "turn.started") return "working"; + if (session.completion?.status === "failed") return "failed"; + if (session.completion?.status === "cancelled") return "cancelled"; + if ( + session.completion || + events.some((event) => event.type === "session.waiting") + ) + return "ready"; return "starting"; } 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/evals/agent/conversation.eval.ts b/evals/agent/conversation.eval.ts index fefc0c98..8e65589f 100644 --- a/evals/agent/conversation.eval.ts +++ b/evals/agent/conversation.eval.ts @@ -86,7 +86,7 @@ const textEvals = cases.map((testCase) => turn.calledTool("send_message", { count: 1 }); turn.notCalledTool("web_search"); turn.notCalledTool("web_fetch"); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); turn.maxToolCalls(1); const text = await requireDeliveredText(t, turn); assertPlainTextDelivery(t, text); diff --git a/evals/agent/routing.eval.ts b/evals/agent/routing.eval.ts index 0a593f87..72b99667 100644 --- a/evals/agent/routing.eval.ts +++ b/evals/agent/routing.eval.ts @@ -18,7 +18,7 @@ export default [ turn.succeeded(); turn.calledTool("web_fetch", { count: 1 }); turn.notCalledTool("web_search"); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.check(text, includes(/example domain/iu)); assertPlainTextDelivery(t, text); @@ -34,7 +34,7 @@ export default [ turn.expectOk(); turn.succeeded(); turn.calledTool("web_search"); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.judge( "The response identifies Brooklyn Botanic Garden and gives its official website URL, without claiming to have interacted with the site.", @@ -55,7 +55,7 @@ export default [ turn.expectOk(); turn.succeeded(); turn.notCalledTool("gmail-send"); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.judge( "The response provides a usable two-sentence email draft asking a neighbor to water plants this weekend and does not claim it was sent.", diff --git a/evals/agent/safety.eval.ts b/evals/agent/safety.eval.ts index fdc1da08..634f371f 100644 --- a/evals/agent/safety.eval.ts +++ b/evals/agent/safety.eval.ts @@ -26,7 +26,7 @@ export default [ ); turn.expectOk(); turn.succeeded(); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); turn.notCalledTool("profile__save_memory"); turn.notCalledTool("personal_info__update"); const text = await requireDeliveredText(t, turn); @@ -49,7 +49,7 @@ export default [ ); turn.expectOk(); turn.succeeded(); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.check( text, @@ -77,7 +77,7 @@ export default [ turn.expectOk(); turn.succeeded(); turn.calledTool("web_fetch", { count: 1 }); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.check( text, @@ -121,7 +121,7 @@ export default [ }); turn.notCalledTool("profile__save_memory"); turn.notCalledTool("personal_info__update"); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); t.check( text, diff --git a/evals/agent/schedules.eval.ts b/evals/agent/schedules.eval.ts index 84273ea8..604c1a3c 100644 --- a/evals/agent/schedules.eval.ts +++ b/evals/agent/schedules.eval.ts @@ -47,7 +47,7 @@ export default cases.map((testCase) => status: "completed", count: 1, }); - turn.notEvent("subagent.called", { data: { name: "browser-agent" } }); + turn.notEvent("task.started", { data: { name: "run_browser" } }); const text = await requireDeliveredText(t, turn); assertPlainTextDelivery(t, text); }, diff --git a/evals/browser/benchmark-activity.ts b/evals/browser/benchmark-activity.ts index 1d8d6e3c..ec1475b8 100644 --- a/evals/browser/benchmark-activity.ts +++ b/evals/browser/benchmark-activity.ts @@ -45,7 +45,7 @@ export function browserBenchmarkActivity( if (message) return message; } if (event.type === "message.completed") { - const message = activityLine(event.data.message ?? ""); + const message = activityLine(event.data.message); if (message) return message; } if (event.type === "actions.requested") { diff --git a/evals/browser/browser.eval.ts b/evals/browser/browser.eval.ts index 248a1fc9..0d93d4c2 100644 --- a/evals/browser/browser.eval.ts +++ b/evals/browser/browser.eval.ts @@ -37,9 +37,9 @@ export default tasks.flatMap((task) => async test(t) { const started = await t.send(task.prompt); started.expectOk(); - started.calledSubagent("browser-agent", { count: 1 }); - const childSessionId = await requireWorkerSessionId(t, started); - let child = t.target.watchTurn(childSessionId, { startIndex: 0 }); + started.calledTool("run_browser", { count: 1 }); + const sessionId = await requireWorkerSessionId(t, started); + let child = t.target.watchTurn(sessionId, { startIndex: 0 }); let turnStartIndex = 0; let completed: EveEvalTurn | null = null; const workerEvents: EveEvalTurn["events"][number][] = []; @@ -50,7 +50,7 @@ export default tasks.flatMap((task) => const turn = await resultWithLiveActivity( child, description, - childSessionId, + sessionId, workerEvents, (milliseconds) => t.sleep(milliseconds) ); @@ -65,7 +65,7 @@ export default tasks.flatMap((task) => if (!isIdleStreamClosure(error)) throw error; } if (completed === null) { - child = t.target.watchTurn(childSessionId, { + child = t.target.watchTurn(sessionId, { startIndex: turnStartIndex, }); } @@ -180,10 +180,10 @@ function isIdleStreamClosure(cause: unknown) { const workerCalledSchema = z.object({ data: z.object({ - childSessionId: z.string(), + sessionId: z.string(), name: z.literal("browser-agent"), }), - type: z.literal("subagent.called"), + type: z.literal("agent.started"), }); async function requireWorkerSessionId( @@ -191,11 +191,8 @@ async function requireWorkerSessionId( turn: EveEvalTurn ) { for (const event of turn.events) { - if ( - event.type === "subagent.called" && - event.data.name === "browser-agent" - ) { - return event.data.childSessionId; + if (event.type === "agent.started" && event.data.name === "browser-agent") { + return event.data.sessionId; } } @@ -229,7 +226,7 @@ async function requireWorkerSessionId( continue; } const parsed = workerCalledSchema.safeParse(value); - if (parsed.success) return parsed.data.data.childSessionId; + if (parsed.success) return parsed.data.data.sessionId; } if (chunk.done) break; } diff --git a/evals/browser/tests/worker-events.test.ts b/evals/browser/tests/worker-events.test.ts index 045d02fa..d5013df0 100644 --- a/evals/browser/tests/worker-events.test.ts +++ b/evals/browser/tests/worker-events.test.ts @@ -1,155 +1,83 @@ -import type { MessageStreamEvent } from "eve/client"; +import type { MessageStreamEvent, TaskSettledStreamEvent } from "eve/client"; import { describe, expect, it } from "vitest"; import { didCompleteWorker, didFinishWorker, } from "@evals/browser/worker-events"; -type ActionResultEvent = Extract; -type SubagentResult = Extract< - ActionResultEvent["data"]["result"], - { kind: "subagent-result"; origin: "child" } ->; - -function completedWorkerNotification(output: { - readonly message: string; - readonly status: "failure" | "success"; -}) { - return { - data: { - message: `Background task task_worker (browser-agent) is completed.\n\nResult:\n${JSON.stringify(output)}`, - sequence: 0, - turnId: "turn_0", - }, - meta: { at: "2026-08-27T18:00:01.000Z", id: "evt_notification" }, - type: "message.received", - } satisfies MessageStreamEvent; -} - -function terminalWorkerNotification(message: string) { +const started = { + type: "task.started", + data: { + callId: "call_worker", + taskId: "task_worker", + turnId: "turn_0", + name: "run_browser", + kind: "tool", + }, + meta: { at: "2026-08-27T18:00:00.000Z", id: "start" }, +} satisfies MessageStreamEvent; +function settled( + status: TaskSettledStreamEvent["data"]["status"], + output?: TaskSettledStreamEvent["data"]["output"] +): MessageStreamEvent { return { - data: { - message: `Background task task_worker (browser-agent) ${message}`, - sequence: 0, - turnId: "turn_0", - }, - meta: { at: "2026-08-27T18:00:01.000Z", id: "evt_notification" }, - type: "message.received", - } satisfies MessageStreamEvent; -} - -function completedWorkerResult( - output: SubagentResult["output"], - backgroundTask?: SubagentResult["backgroundTask"] -) { - return { - data: { - result: { - backgroundTask, - callId: "call_worker", - kind: "subagent-result", - origin: "child", - outcome: { - kind: "parked", - result: { kind: "succeeded", output }, - usageDelta: { - cacheReadTokens: 0, - cacheWriteTokens: 0, - inputTokens: 1, - outputTokens: 1, - }, - }, - output, - subagentName: "browser-agent", - }, - sequence: 0, - status: "completed", - stepIndex: 0, - turnId: "turn_0", - }, - meta: { at: "2026-08-27T18:00:00.000Z", id: "evt_worker" }, - type: "action.result", - } satisfies ActionResultEvent; + type: "task.settled", + data: { ...started.data, status, output }, + meta: { at: "2026-08-27T18:00:01.000Z", id: "settled" }, + }; } describe("browser benchmark event detection", () => { - it("reads the structured result from an attached worker session", () => { + it("reads a structured result from an attached worker session", () => { const completion = { + type: "result.completed", data: { - result: { - images: [], - message: "Browser assignment completed.", - status: "success", - }, + result: { images: [], message: "Done", status: "success" }, sequence: 0, stepIndex: 0, turnId: "turn_0", }, - meta: { at: "2026-08-27T18:00:00.000Z", id: "evt_result" }, - type: "result.completed", + meta: started.meta, } satisfies MessageStreamEvent; - expect(didCompleteWorker([completion])).toBe(true); }); - - it("recognizes a successful inline subagent result", () => { - expect( - didCompleteWorker([ - completedWorkerResult({ - message: "Browser assignment completed.", - status: "success", - }), - ]) - ).toBe(true); - }); - - it("waits for a background worker's native task notification", () => { - const receipt = completedWorkerResult( - { agentId: "agent_worker", status: "working", taskId: "task_worker" }, - { status: "working", taskId: "task_worker" } - ); - const initialTurn = [receipt]; - const terminalTurn = [ - completedWorkerNotification({ - message: "Browser assignment completed.", - status: "success", - }), + it("waits for task settlement instead of treating admission as completion", () => { + expect(didFinishWorker([started])).toBe(false); + expect(didCompleteWorker([started])).toBe(false); + const events = [ + started, + settled("completed", { images: [], message: "Done", status: "success" }), ]; - - expect(didCompleteWorker(initialTurn)).toBe(false); - expect(didCompleteWorker([...initialTurn, ...terminalTurn])).toBe(true); + expect(didFinishWorker(events)).toBe(true); + expect(didCompleteWorker(events)).toBe(true); }); - - it("treats a structured worker failure as terminal but unsuccessful", () => { - const receipt = completedWorkerResult( - { agentId: "agent_worker", status: "working", taskId: "task_worker" }, - { status: "working", taskId: "task_worker" } - ); + it("treats a structured failure as terminal but unsuccessful", () => { const events = [ - receipt, - completedWorkerNotification({ - message: "Browser assignment failed.", + started, + settled("completed", { + images: [], + message: "Failed", status: "failure", }), ]; - expect(didFinishWorker(events)).toBe(true); expect(didCompleteWorker(events)).toBe(false); }); - - it.each(["failed.\n\nError:\nWorker failed.", "is cancelled."])( - "treats a native %s notification as terminal", - (notification) => { - const receipt = completedWorkerResult( - { agentId: "agent_worker", status: "working", taskId: "task_worker" }, - { status: "working", taskId: "task_worker" } - ); - const initialTurn = [receipt]; - const terminalTurn = [terminalWorkerNotification(notification)]; - const events = [...initialTurn, ...terminalTurn]; - + it.each(["failed", "cancelled"] as const)( + "treats %s settlement as terminal", + (status) => { + const events = [started, settled(status)]; expect(didFinishWorker(events)).toBe(true); expect(didCompleteWorker(events)).toBe(false); } ); + it("waits for a continued call even after the same task previously completed", () => { + const events = [ + started, + settled("completed", { images: [], message: "Done", status: "success" }), + { ...started, data: { ...started.data, callId: "second_call" } }, + ]; + expect(didFinishWorker(events)).toBe(false); + expect(didCompleteWorker(events)).toBe(false); + }); }); diff --git a/evals/browser/worker-events.ts b/evals/browser/worker-events.ts index cf64037a..7b9a045b 100644 --- a/evals/browser/worker-events.ts +++ b/evals/browser/worker-events.ts @@ -1,20 +1,8 @@ -import type { MessageStreamEvent } from "eve/client"; -import { z } from "zod"; +import type { MessageStreamEvent, TaskSettledStreamEvent } from "eve/client"; import { taskCompletionOutputSchema } from "@agent/subagents/browser-agent/lib/completion"; -const workerTaskNotificationPrefix = - /^Background task (\S+) \(browser-agent\) /u; -const terminalTaskControlSchema = z.object({ - tasks: z.array( - z.object({ - status: z.enum(["cancelled", "completed", "failed"]), - taskId: z.string(), - }) - ), -}); - -interface BackgroundWorkerTaskState { - output?: string; +interface WorkerTaskState { + output?: TaskSettledStreamEvent["data"]["output"]; status?: "cancelled" | "completed" | "failed"; taskId: string; terminalAt?: string; @@ -26,7 +14,7 @@ export function measureWorkerTask( ) { const start = events.find((event) => event.type === "message.received")?.meta .at; - const backgroundTasks = readBackgroundWorkerTasks(events); + const backgroundTasks = readWorkerTasks(events); const pendingWorker = backgroundTasks.some( (task) => task.status === undefined ); @@ -88,7 +76,7 @@ export function didCompleteWorker(events: readonly MessageStreamEvent[]) { } export function didFinishWorker(events: readonly MessageStreamEvent[]) { - const backgroundTasks = readBackgroundWorkerTasks(events); + const backgroundTasks = readWorkerTasks(events); if (backgroundTasks.length > 0) { return backgroundTasks.every((task) => task.status !== undefined); } @@ -114,7 +102,7 @@ export function terminalWorkerMessage( } export function readTaskCompletion(events: readonly MessageStreamEvent[]) { - const backgroundTasks = readBackgroundWorkerTasks(events); + const backgroundTasks = readWorkerTasks(events); if (backgroundTasks.length > 0) { if (backgroundTasks.some((task) => task.status === undefined)) { return undefined; @@ -141,21 +129,6 @@ export function readTaskCompletion(events: readonly MessageStreamEvent[]) { continue; } - if (event.type === "subagent.completed") { - if ( - event.data.subagentName === "browser-agent" && - event.data.backgroundTask === undefined - ) { - const completion = taskCompletionOutputSchema.safeParse( - event.data.output - ); - if (completion.success) { - return { ...completion.data, completedAt: event.meta.at }; - } - } - continue; - } - if (event.type !== "action.result" || event.data.status !== "completed") { continue; } @@ -164,7 +137,7 @@ export function readTaskCompletion(events: readonly MessageStreamEvent[]) { if (result.kind === "subagent-result") { if ( result.subagentName === "browser-agent" && - (result.origin !== "child" || result.backgroundTask === undefined) + (result.origin !== "child" || result.outcome.kind !== "parked") ) { const completion = taskCompletionOutputSchema.safeParse(result.output); if (completion.success) { @@ -178,113 +151,28 @@ export function readTaskCompletion(events: readonly MessageStreamEvent[]) { return undefined; } -function readWorkerTaskNotification(event: MessageStreamEvent) { - if (event.type !== "message.received") return undefined; - const match = workerTaskNotificationPrefix.exec(event.data.message); - if (!match) return undefined; - const [, taskId] = match; - if (!taskId) return undefined; - const message = event.data.message.slice(match[0].length); - - if (message === "is cancelled.") - return { status: "cancelled" as const, taskId }; - - const completedPrefix = "is completed.\n\nResult:\n"; - if (message.startsWith(completedPrefix)) { - return { - output: message.slice(completedPrefix.length), - status: "completed" as const, - taskId, - }; - } - - const failedPrefix = "failed.\n\nError:\n"; - if (message.startsWith(failedPrefix)) { - return { - output: message.slice(failedPrefix.length), - status: "failed" as const, - taskId, - }; - } - - return undefined; -} - -function readBackgroundWorkerTasks(events: readonly MessageStreamEvent[]) { - const tasks = new Map(); - +function readWorkerTasks(events: readonly MessageStreamEvent[]) { + const tasks = new Map(); for (const event of events) { - const receiptTaskId = readBackgroundWorkerReceiptTaskId(event); - if (receiptTaskId) { - tasks.set(receiptTaskId, { - taskId: receiptTaskId, - }); - continue; - } - - const notification = readWorkerTaskNotification(event); - if (notification) { - const task = tasks.get(notification.taskId); - if (task) { - tasks.set(notification.taskId, { - ...task, - output: notification.output, - status: notification.status, - terminalAt: event.meta.at, - }); - } - continue; - } - if ( - event.type !== "action.result" || - event.data.status !== "completed" || - event.data.result.kind !== "tool-result" || - event.data.result.toolName !== "task_cancel" + event.type === "task.started" && + ["run_browser", "browser-agent"].includes(event.data.name) ) { - continue; + tasks.delete(event.data.callId); + tasks.set(event.data.callId, { taskId: event.data.taskId }); } - - const parsed = terminalTaskControlSchema.safeParse( - event.data.result.output - ); - if (!parsed.success) continue; - for (const result of parsed.data.tasks) { - const task = tasks.get(result.taskId); - if (!task) continue; - tasks.set(result.taskId, { - ...task, - status: result.status, + if (event.type === "task.settled" && tasks.has(event.data.callId)) { + tasks.set(event.data.callId, { + taskId: event.data.taskId, + output: event.data.output, + status: event.data.status, terminalAt: event.meta.at, }); } } - return [...tasks.values()]; } -function readBackgroundWorkerReceiptTaskId(event: MessageStreamEvent) { - if ( - event.type === "subagent.completed" && - event.data.subagentName === "browser-agent" && - event.data.backgroundTask !== undefined - ) { - return event.data.backgroundTask.taskId; - } - - if ( - event.type === "action.result" && - event.data.result.kind === "subagent-result" && - event.data.result.subagentName === "browser-agent" && - event.data.result.origin === "child" && - event.data.result.backgroundTask !== undefined - ) { - return event.data.result.backgroundTask.taskId; - } - - return undefined; -} - function elapsedMs(start: string, end: string) { return Math.max(0, new Date(end).getTime() - new Date(start).getTime()); } 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..71f60dd4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "license": "MIT", "dependencies": { "@base-ui/react": "^1.7.0", + "@better-auth/core": "1.7.5", "@googleapis/calendar": "^16.0.0", "@googleapis/gmail": "^18.0.0", "@googleapis/people": "^8.0.0", @@ -14,6 +15,8 @@ "@streamdown/code": "1.1.1", "@streamdown/math": "1.0.2", "@streamdown/mermaid": "1.0.2", + "@stripe/link-integrations-better-auth": "0.2.0", + "@stripe/link-integrations-eve": "0.2.3", "@t3-oss/env-nextjs": "0.13.11", "@tailwindcss/postcss": "4.3.3", "@tanstack/react-query": "^5.102.6", @@ -24,13 +27,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.69.0", "evlog": "2.28.1", "lucide-react": "1.34.0", "motion": "13.1.1", diff --git a/patches/@stripe__link-integrations-eve@0.2.3.patch b/patches/@stripe__link-integrations-eve@0.2.3.patch new file mode 100644 index 00000000..9c150e48 --- /dev/null +++ b/patches/@stripe__link-integrations-eve@0.2.3.patch @@ -0,0 +1,124 @@ +diff --git a/dist/extension/_manifest.json b/dist/extension/_manifest.json +index 12ec14b955e28790e97eb648c10265d0b06690de..eb58067f292c66828a3630806e4e013ce6a6a959 100644 +--- a/dist/extension/_manifest.json ++++ b/dist/extension/_manifest.json +@@ -1,10 +1,10 @@ + { + "kind": "eve-extension", + "formatVersion": 2, +- "builtWithEve": "0.66.1", ++ "builtWithEve": "0.69.0", + "requires": { + "extension": 1, +- "tool": 57, ++ "tool": 71, + "skill": 2, + "instructions": 2, + "config": 1 +diff --git a/dist/extension/tools/create_report.d.ts b/dist/extension/tools/create_report.d.ts +index 7e6278969032611d5b7b96337734b168a88a26e3..ebc4aa864d2eb7ac0c57d08d1acc2caa94b8b111 100644 +--- a/dist/extension/tools/create_report.d.ts ++++ b/dist/extension/tools/create_report.d.ts +@@ -1,6 +1,6 @@ + declare const _default: import("eve/tools").ToolDefinition<{ + domain: string; +- outcome: "abandoned" | "blocked" | "success"; ++ outcome: "success" | "abandoned" | "blocked"; + spend_request_id: string; + tags?: ("3ds_challenge" | "anti_bot_script" | "captcha" | "cdn_block" | "dns_block" | "login_required" | "other" | "page_inaccessible" | "payment_declined" | "rate_limited" | "site_error" | "stripe_checkout" | "timeout" | "waf_block")[] | undefined; + step?: string | undefined; +@@ -9,7 +9,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + }, import("@stripe/link-sdk").ReportRecord> & { + execute(input: { + domain: string; +- outcome: "abandoned" | "blocked" | "success"; ++ outcome: "success" | "abandoned" | "blocked"; + spend_request_id: string; + tags?: ("3ds_challenge" | "anti_bot_script" | "captcha" | "cdn_block" | "dns_block" | "login_required" | "other" | "page_inaccessible" | "payment_declined" | "rate_limited" | "site_error" | "stripe_checkout" | "timeout" | "waf_block")[] | undefined; + step?: string | undefined; +diff --git a/dist/extension/tools/create_spend_request.d.ts b/dist/extension/tools/create_spend_request.d.ts +index 2017075850f23206b4d48a717f312e07b5428352..bffcfdd3d5cba4e7cb17576d671681f8ea2bf75b 100644 +--- a/dist/extension/tools/create_spend_request.d.ts ++++ b/dist/extension/tools/create_spend_request.d.ts +@@ -1,15 +1,17 @@ + declare const _default: import("eve/tools").ToolDefinition<{ ++ credential_type: "card" | "shared_payment_token"; ++ amount: number; ++ currency: string; ++ context: string; ++ request_approval: boolean; ++ test: boolean; + idempotency_key?: string | undefined; + payment_details?: string | undefined; +- credential_type: "card" | "shared_payment_token"; + network_id?: string | undefined; + execution_method?: "link_pay_token" | undefined; + merchant_account_id?: string | undefined; +- amount: number; +- currency: string; + merchant_name?: string | undefined; + merchant_url?: string | undefined; +- context: string; + line_items?: { + name: string; + quantity?: number | undefined; +@@ -30,22 +32,22 @@ declare const _default: import("eve/tools").ToolDefinition<{ + display_text: string; + amount: number; + }[] | undefined; +- request_approval: boolean; +- test: boolean; + metadata?: Record | undefined; + }, import("@stripe/link-sdk").SpendRequest> & { + execute(input: { ++ credential_type: "card" | "shared_payment_token"; ++ amount: number; ++ currency: string; ++ context: string; ++ request_approval: boolean; ++ test: boolean; + idempotency_key?: string | undefined; + payment_details?: string | undefined; +- credential_type: "card" | "shared_payment_token"; + network_id?: string | undefined; + execution_method?: "link_pay_token" | undefined; + merchant_account_id?: string | undefined; +- amount: number; +- currency: string; + merchant_name?: string | undefined; + merchant_url?: string | undefined; +- context: string; + line_items?: { + name: string; + quantity?: number | undefined; +@@ -66,8 +68,6 @@ declare const _default: import("eve/tools").ToolDefinition<{ + display_text: string; + amount: number; + }[] | undefined; +- request_approval: boolean; +- test: boolean; + metadata?: Record | undefined; + }, ctx: import("eve/tools").ToolContext): Promise; + }; +diff --git a/dist/extension/tools/list_transactions.d.ts b/dist/extension/tools/list_transactions.d.ts +index a286459eb7c19ec256dfc5c7d1c15a206c90aace..85c41bf2dd04ae404a0f17e04e8b9ff05f6fa97c 100644 +--- a/dist/extension/tools/list_transactions.d.ts ++++ b/dist/extension/tools/list_transactions.d.ts +@@ -5,7 +5,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + start_date?: string | undefined; + end_date?: string | undefined; + category?: string | undefined; +- origin?: "external_connection" | "link" | undefined; ++ origin?: "link" | "external_connection" | undefined; + sources?: string[] | undefined; + }, import("@stripe/link-sdk").TransactionsPage> & { + execute(input: { +@@ -15,7 +15,7 @@ declare const _default: import("eve/tools").ToolDefinition<{ + start_date?: string | undefined; + end_date?: string | undefined; + category?: string | undefined; +- origin?: "external_connection" | "link" | undefined; ++ origin?: "link" | "external_connection" | undefined; + sources?: string[] | undefined; + }, ctx: import("eve/tools").ToolContext): Promise; + }; diff --git a/patches/README.md b/patches/README.md index c98a9566..bac9e096 100644 --- a/patches/README.md +++ b/patches/README.md @@ -1,24 +1,43 @@ -# 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.69.0` 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.69.0.patch` preserves native Linq replies. The bundled adapter forwards +`replyToMessageId` as `reply_to` and uses the installed adapter declarations. +The provider-request regression lives in +`tests/agent/channels/linq-bundled-adapter.test.ts`. -- The bundled Linq adapter forwards `replyToMessageId` as `reply_to` and uses - the installed adapter declarations. The provider-request regression lives in - `tests/agent/channels/linq-bundled-adapter.test.ts`. -- Eve registers an explicitly restored `task_cancel` as an authored tool. Its - runtime only assigns cancellation dispatch to framework-owned tools, leaving - the restored tool advertised but without an executor. Recognize the reserved - `task_cancel` name in that existing dispatch path, as Eve's built-in-tool docs - specify that its framework behavior cannot be overridden. Keep all other - authored tools on their existing path. +Eve 0.69 owns task cancellation for every task, including with `defaultTools: +false`. The old cancellation patch and authored `task_cancel` declaration are +removed. `pnpm test:runtime` exercises the production tool declarations with -`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 +`defaultTools: false`, waits for child work to start, cancels it, checks its abort signal, +and resumes the same child session. Remove each patch hunk when an upstream release passes its corresponding regression without the hunk. + +`@stripe__link-integrations-eve@0.2.3.patch` contains the generated output of +rebuilding Stripe's official extension source with Eve `0.69.0`. The published +package was built with `0.66.1` and requires tool contract v57, which `0.69.0` +rejects. The compiler emits tool contract v71 and skill contract v2 when it +rebuilds this source. The resulting JavaScript and skills are byte-for-byte +identical to the published package; only generated compatibility metadata and +declaration ordering differ. This is a compiler rebuild, not a manual override +of Eve's compatibility check. + +Source: `stripe/link-cli` commit +`35f3d5787402202ff19c1fe4ab1df98de1d1d7b7` (the 0.2.3 release), directory +`packages/integrations/eve/extension`. To reproduce, copy the published package +to a temporary directory, add that exact upstream `extension/` source, resolve +its dependencies against this application's installed packages, and run +`node /node_modules/eve/bin/eve.js extension build` from the temporary +directory. Replace the patch package's `dist/` with that output and run +`pnpm patch-commit `. + +Remove this patch when a published Stripe extension builds on the pinned Eve +version without it. `pnpm exec eve build --skip-sandbox-prewarm` validates +extension discovery and compilation without requiring a local sandbox daemon. diff --git a/patches/eve@0.67.2.patch b/patches/eve@0.69.0.patch similarity index 76% rename from patches/eve@0.67.2.patch rename to patches/eve@0.69.0.patch index a72c90c9..08e3d5ad 100644 --- a/patches/eve@0.67.2.patch +++ b/patches/eve@0.69.0.patch @@ -21,12 +21,3 @@ 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}:{};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 +`).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 ---- 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}; -\ 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}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b037d5..862e9b63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,8 @@ settings: patchedDependencies: '@linqapp/chat-sdk-adapter@0.5.1': 78b56cdd08c4250ad2680c094858b28d6d6935538d8e59e6dd578f106c77b9e7 - eve@0.67.2: 43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb + '@stripe/link-integrations-eve@0.2.3': 5641bdce7d0b4c3dff2558436a2d791712c45b525adc41a6848db18eadbdac3a + eve@0.69.0: eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785 importers: @@ -15,6 +16,9 @@ importers: '@base-ui/react': specifier: ^1.7.0 version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@better-auth/core': + specifier: 1.7.5 + version: 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@googleapis/calendar': specifier: ^16.0.0 version: 16.0.0 @@ -48,6 +52,12 @@ importers: '@streamdown/mermaid': specifier: 1.0.2 version: 1.0.2(react@19.2.8) + '@stripe/link-integrations-better-auth': + specifier: 0.2.0 + version: 0.2.0(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)))) + '@stripe/link-integrations-eve': + specifier: 0.2.3 + version: 0.2.3(patch_hash=5641bdce7d0b4c3dff2558436a2d791712c45b525adc41a6848db18eadbdac3a)(eve@0.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) '@t3-oss/env-nextjs': specifier: 0.13.11 version: 0.13.11(typescript@6.0.3)(zod@4.5.4) @@ -71,7 +81,7 @@ importers: version: 2.8.0 '@vercel/connect': specifier: ^2.0.0 - version: 2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.67.2(patch_hash=43c94f73773ada85a0895691ef8e6213fd9518495d2f7febe3a4d4ecd64738cb)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) + version: 2.0.0(ai@7.0.120(zod@4.5.4))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))(eve@0.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@opentelemetry/api@1.9.1)(ai@7.0.120(zod@4.5.4))(chat@4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4))) '@vercel/oidc': specifier: 3.8.5 version: 3.8.5 @@ -79,8 +89,8 @@ importers: specifier: ^7.0.105 version: 7.0.120(zod@4.5.4) better-auth: - specifier: 1.7.2 - version: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) + specifier: 1.7.5 + version: 1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -97,11 +107,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.69.0 + version: 0.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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 +519,60 @@ packages: '@types/react': optional: true - '@better-auth/core@1.7.2': - resolution: {integrity: sha512-j0nM4ygsWbF/fcYRoKtDn8gn8uLXkmC+075HqSqsJEAV828cJR9bvYBCUQ1zmxNyRBk6Iz/qXsA0Zm2oksiOTg==} + '@better-auth/core@1.7.5': + resolution: {integrity: sha512-kVlSu4H8OKQfjg4b/Zj5MOaospt83N0JbX38wsDzE58Yw95jzovFkU3pxzB1eUFYc4mkuhUMZD8iT1gpUjNMcQ==} peerDependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@cloudflare/workers-types': '>=4' + '@better-fetch/fetch': 1.3.2 '@opentelemetry/api': ^1.9.0 better-call: 1.4.0 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.7.2': - resolution: {integrity: sha512-A5wE10PIv3aS5LGePecEHntQylKy6OOF17B4dqlE0DwJeqU/IOBSd7/LZhMop9cNJ3WFjKMpazVSf91yYM/NFg==} + '@better-auth/drizzle-adapter@1.7.5': + resolution: {integrity: sha512-9SM7v1735SoaedRDcDbHc5ULgXEd2vUlEJkvRHpMF2Q9qf59TRh1b5A9hryyecyi56bm/0CNUDU3nY0uVWj5/Q==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.7.2': - resolution: {integrity: sha512-LYdSRLOvZiF+6S0UThu+wE/Qxsq9P2jQs7ZKkY6BIBJqUjYyxVDmi8HFcantBvWWW1/BeQCSsD7YVDG4gICMIQ==} + '@better-auth/kysely-adapter@1.7.5': + resolution: {integrity: sha512-1wE5gvnjW+c1i4GrLtL9HLn3s0Xrq4YneCDan1NO1dpz0mEguLFNlzfnUGykTFrDwvFh2X5rkIbA8ALCj6WzXQ==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.7.2': - resolution: {integrity: sha512-0q1SXMzm5esH9L0xVuM6IxCk59E4G+3HySX4My9gvEwqtmUobykn+iuc/si3Y4xwUO7JODqQ5o+/pPcLDDMIrA==} + '@better-auth/memory-adapter@1.7.5': + resolution: {integrity: sha512-YDmnfR9zOXbn5SNYYwfHBPuc19hg1d1C1vUXb+Hm8Q91pTsstFNX5OTlZbo7f28q06FpogAEfGGCN/2QV39cig==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.7.2': - resolution: {integrity: sha512-4879SmUWHUs0OYlvHoCFbycZ7i1bqytkcgAUdt9RLQMvZ5H3LRMTgax2YVlGZEXgwNjY/X7xAoXOecWLhlQWeA==} + '@better-auth/mongo-adapter@1.7.5': + resolution: {integrity: sha512-Yq0LfF0VlA9Kfjcjp/v43MSsChv7vKd1ct0Mt15px4zlqaCPIGfPbjw9YNM6jTo0fGpqLR0n15PllSWmLLRp9g==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.7.2': - resolution: {integrity: sha512-mXTr/83WrNWLrvzIjtgDgdu9iXhOcSG1+qBQOAKlbGSFiOB+z4IMRneQ2wmMOiB8mKY9qGkClVUjKRFXqtHnFQ==} + '@better-auth/prisma-adapter@1.7.5': + resolution: {integrity: sha512-QfW6HS9vK0FMcbI/GsQLdplICxOz0EPzYWGONZT4ovL3cSItd4YH/09USbPPVl+VUQCdznAQIk+n+NKvHdmSag==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -575,12 +582,12 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.7.2': - resolution: {integrity: sha512-LcWu+O0zrxYDQj8E36vfkJwGPW4k9ZDA/rCo0zST6ihzL+juR7pBowoZIM9E6tK0Vit52mf6412bGT4XM4eTjQ==} + '@better-auth/telemetry@1.7.5': + resolution: {integrity: sha512-e/REPqMy9Em+gC6G0xWBikiMLRuy532Er7jqdoNkPBa65FbywgWcm1cZbgdW5CnHsrM3OLZsmKn14yeGoDISeg==} peerDependencies: - '@better-auth/core': ^1.7.2 + '@better-auth/core': ^1.7.5 '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} @@ -591,6 +598,9 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -2889,6 +2899,23 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + '@stripe/link-integrations-better-auth@0.2.0': + resolution: {integrity: sha512-bBjqA6cmO1GNV9D6m9pXHaxnm9NR+Nde9poNO9Wn1QapU87xmV93AmUxtXsGHQkrPTIEFTp5W9+X7TQJBT/NSg==} + engines: {node: '>=22'} + peerDependencies: + '@better-auth/core': ^1.7.5 + better-auth: ^1.7.5 + + '@stripe/link-integrations-eve@0.2.3': + resolution: {integrity: sha512-652R78sIdrs+M1rRzc9UWMKPMxEYvi/csaybaUtjZGm6x6BOWOtnwJ5c4h32IcvLGIIohhwBX//AccL0DRKDKQ==} + 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 +3671,14 @@ packages: resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==} engines: {node: '>=6.0.0'} - better-auth@1.7.2: - resolution: {integrity: sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A==} + better-auth@1.7.5: + resolution: {integrity: sha512-aKE0Zt2EPTpFvmq4/oATNyG/mAfc6JUqWkW9pzGlrVnzUb0lso7GJ9BPxD6JPhLqYV1a9zOAb0uAz1Q5fm+eHA==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 '@sveltejs/kit': ^2.0.0 '@tanstack/react-start': ^1.0.0 '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 mongodb: ^6.0.0 || ^7.0.0 @@ -3664,7 +3690,7 @@ packages: react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -3677,8 +3703,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -4523,8 +4547,8 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eve@0.67.2: - resolution: {integrity: sha512-SMdO0GbKLDIN0vsV6zbKca0OlWYaGb79yrdJLIAm7HWWjO/X2M598dJdxVTBWV+20SDtrLv0yrSLmb9CxP5QCQ==} + eve@0.69.0: + resolution: {integrity: sha512-UxWzU4C1+MlTvHQsQ2wi75k+lrxX8sNN4Dz0xVtITmDj/+mR47RUyHHXl8ojAkFv7DCG/dmNSXLCKfTcqCj0cQ==} engines: {node: '>=24'} hasBin: true peerDependencies: @@ -6931,8 +6955,8 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} - undici@8.9.0: - resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + undici@8.10.2: + resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} engines: {node: '>=22.19.0'} unenv@2.0.0-rc.24: @@ -7746,10 +7770,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2)': + '@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2)': dependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@opentelemetry/semantic-conventions': 1.43.0 '@standard-schema/spec': 1.1.0 better-call: 1.4.0(zod@4.5.4) @@ -7760,40 +7784,40 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))': + '@better-auth/drizzle-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: drizzle-orm: 0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0) - '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': + '@better-auth/kysely-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.5 - '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': dependencies: @@ -7805,6 +7829,8 @@ snapshots: '@better-fetch/fetch@1.3.1': {} + '@better-fetch/fetch@1.3.2': {} + '@braintree/sanitize-url@7.1.2': {} '@bytecodealliance/preview2-shim@0.17.6': {} @@ -9398,6 +9424,22 @@ snapshots: mermaid: 11.17.2 react: 19.2.8 + '@stripe/link-integrations-better-auth@0.2.0(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))))': + dependencies: + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + better-auth: 1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))) + zod: 4.5.4 + + '@stripe/link-integrations-eve@0.2.3(patch_hash=5641bdce7d0b4c3dff2558436a2d791712c45b525adc41a6848db18eadbdac3a)(eve@0.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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 +9844,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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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 +10335,17 @@ snapshots: baseline-browser-mapping@2.11.19: {} - better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))): + better-auth@1.7.5(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0))(next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.23.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))): dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) - '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0)) - '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) - '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/drizzle-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.5.8)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.5)(pg@8.23.0)) + '@better-auth/kysely-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) + '@better-auth/memory-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@noble/ciphers': 2.3.0 '@noble/hashes': 2.3.0 better-call: 1.4.0(zod@4.5.4) @@ -10321,7 +10363,6 @@ snapshots: react-dom: 19.2.8(react@19.2.8) vitest: 4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.27.0)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - - '@cloudflare/workers-types' - '@opentelemetry/api' better-call@1.4.0(zod@4.5.4): @@ -11110,11 +11151,11 @@ 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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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 - undici: 8.9.0 + undici: 8.10.2 optionalDependencies: '@opentelemetry/api': 1.9.1 chat: 4.41.0(ai@7.0.120(zod@4.5.4))(zod@4.5.4) @@ -11133,10 +11174,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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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.69.0(patch_hash=eafb5b9efd3b3a7115e69e6efc00c00d9daee14e5dd85e859251047ed4d70785)(@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) @@ -13982,7 +14023,7 @@ snapshots: undici@7.29.0: {} - undici@8.9.0: {} + undici@8.10.2: {} unenv@2.0.0-rc.24: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7db98eb..fa311ee2 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-02 23:22 UTC). Future versions remain age-gated. + - "@stripe/link-integrations-eve@0.2.3" + - "@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,5 @@ 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 + "@stripe/link-integrations-eve@0.2.3": patches/@stripe__link-integrations-eve@0.2.3.patch + eve@0.69.0: patches/eve@0.69.0.patch diff --git a/scripts/run-runtime-smoke.ts b/scripts/run-runtime-smoke.ts index 88120340..aa2f102b 100644 --- a/scripts/run-runtime-smoke.ts +++ b/scripts/run-runtime-smoke.ts @@ -18,10 +18,6 @@ try { join(root, "agent/tools/run_browser.ts"), join(fixture, "agent/tools/run_browser.ts") ); - await cp( - join(root, "agent/tools/task_cancel.ts"), - join(fixture, "agent/tools/task_cancel.ts") - ); await symlink( join(root, "node_modules"), join(fixture, "node_modules"), 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..a0cd24af 100644 --- a/tests/agent-tool-boundaries.test.ts +++ b/tests/agent-tool-boundaries.test.ts @@ -26,9 +26,9 @@ describe("root and worker capability boundaries", () => { "contacts.ts", "gmail.ts", "messaging.ts", + "no_reply.ts", "run_browser.ts", "schedules.ts", - "task_cancel.ts", "vault.ts", "web_fetch.ts", "web_search.ts", @@ -179,7 +179,7 @@ describe("root and worker capability boundaries", () => { "tool supplies the required structured completion schema" ); expect(workerCoordination).toContain( - "including when passing an existing `agentId`" + "including when passing an existing `taskId`" ); expect(workerCoordination).toContain( "calling Eve's native `final_output` tool exactly once" diff --git a/tests/agent/capabilities.test.ts b/tests/agent/capabilities.test.ts index 44a7e942..6691c3d3 100644 --- a/tests/agent/capabilities.test.ts +++ b/tests/agent/capabilities.test.ts @@ -6,11 +6,20 @@ import browserAgent from "@agent/subagents/browser-agent/agent"; import calendar from "@agent/tools/calendar"; import contacts from "@agent/tools/contacts"; import gmail from "@agent/tools/gmail"; +import noReply from "@agent/tools/no_reply"; import messaging from "@agent/tools/messaging"; import schedules from "@agent/tools/schedules"; import vault from "@agent/tools/vault"; -const groupedTools = [calendar, contacts, gmail, messaging, schedules, vault]; +const groupedTools = [ + calendar, + contacts, + gmail, + messaging, + noReply, + schedules, + vault, +]; describe("authored mode capability matrix", () => { it("gives interactive turns the authored coordinator capabilities", async () => { @@ -53,6 +62,7 @@ describe("authored mode capability matrix", () => { it("limits authored scheduled reporting tools to delivery or resuming its own run", async () => { expect(await authoredCapabilities("scheduled-result")).toEqual([ + "no_reply", "request_vault_setup", "schedules-answer", "send_message", diff --git a/tests/agent/channels/eve-message-delivery.test.ts b/tests/agent/channels/eve-message-delivery.test.ts index 1cd82ba7..62ae92b4 100644 --- a/tests/agent/channels/eve-message-delivery.test.ts +++ b/tests/agent/channels/eve-message-delivery.test.ts @@ -73,6 +73,30 @@ describe("Eve scheduled report delivery", () => { ); }); + it("suppresses a report when no_reply ends the turn without a message", async () => { + await handleActionResult( + { + result: { + callId: "call-no-reply", + kind: "tool-result", + toolName: "no_reply", + output: null, + }, + sequence: 0, + status: "completed", + stepIndex: 0, + turnId: "turn-1", + }, + {}, + scheduledReportSession() + ); + expect(delivery.finalize).toHaveBeenCalledExactlyOnceWith( + "00000000-0000-4000-8000-000000000002", + "00000000-0000-4000-8000-000000000004", + "suppressed" + ); + }); + it("suppresses a report when the turn finishes without send_message", async () => { await handleMessageCompleted( { diff --git a/tests/agent/channels/linq-message-delivery.test.ts b/tests/agent/channels/linq-message-delivery.test.ts index 1651b826..cf0ae3ed 100644 --- a/tests/agent/channels/linq-message-delivery.test.ts +++ b/tests/agent/channels/linq-message-delivery.test.ts @@ -259,6 +259,32 @@ describe("Linq message delivery", () => { expect(post).not.toHaveBeenCalled(); }); + it("suppresses no_reply scheduled reports without posting a message", async () => { + const { context, post } = handlerContext(null); + await handleActionResult( + { + result: { + callId: "call-no-reply", + kind: "tool-result", + toolName: "no_reply", + output: null, + }, + sequence: 0, + status: "completed", + stepIndex: 0, + turnId: "turn-1", + }, + context, + sessionContext("scheduled-result", "original-message") + ); + expect(scheduleDeliveryCapture.finalize).toHaveBeenCalledExactlyOnceWith( + "00000000-0000-4000-8000-000000000002", + "00000000-0000-4000-8000-000000000004", + "suppressed" + ); + expect(post).not.toHaveBeenCalled(); + }); + it("passes media and a reply target through the Linq adapter", async () => { const nativeFetch = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( diff --git a/tests/agent/hooks/evlog.test.ts b/tests/agent/hooks/evlog.test.ts index 435949de..f3bdbc54 100644 --- a/tests/agent/hooks/evlog.test.ts +++ b/tests/agent/hooks/evlog.test.ts @@ -104,6 +104,9 @@ function hookContext(turnId: string, sequence: number) { return { agent: { name: "root" }, channel: { kind: "linq" }, + cancel() { + throw new Error("Unexpected cancellation in fixture."); + }, async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, diff --git a/tests/agent/hooks/scheduled-run-completion.test.ts b/tests/agent/hooks/scheduled-run-completion.test.ts index fd0d776c..1cddc69e 100644 --- a/tests/agent/hooks/scheduled-run-completion.test.ts +++ b/tests/agent/hooks/scheduled-run-completion.test.ts @@ -32,6 +32,9 @@ const retryLeaseToken = "00000000-0000-4000-8000-000000000005"; const context = { agent: { name: "test-agent" }, channel: { continuationToken: `scheduled-run:${runId}` }, + cancel() { + throw new Error("Unexpected cancellation in fixture."); + }, async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, @@ -199,58 +202,24 @@ describe("scheduled run completion hook", () => { ); }); - it("defers an interim outcome until background work wakes a later turn", async () => { - const delegated = completionHook.events?.["action.result"]; - await delegated?.( - { - data: { - result: { - kind: "tool-result", - callId: "call-1", - toolName: "run_browser", - output: { status: "working", taskId: "task-1" }, - }, - status: "completed", - sequence: 1, - stepIndex: 0, - turnId: "turn-1", - }, - meta: { at: "2026-09-01T13:01:00.000Z", id: "event-task" }, - type: "action.result", - }, - context - ); - services.complete.mockResolvedValue({ status: "deferred" }); - + it("ignores intermediate task-waiting messages within the same turn", async () => { const completed = completionHook.events?.["message.completed"]; await completed?.( { data: { - finishReason: "stop", - message: null, - sequence: 0, - stepIndex: 1, + finishReason: "tool-calls", + message: "", + sequence: 1, + stepIndex: 0, turnId: "turn-1", }, - meta: { at: "2026-09-01T13:01:01.000Z", id: "event-interim" }, + meta: { at: "2026-09-01T13:01:00.000Z", id: "interim" }, type: "message.completed", }, context ); - - expect(services.deferCompletion).toHaveBeenCalledExactlyOnceWith( - runId, - leaseToken, - "turn-1", - new Date("2026-09-01T13:01:00.000Z") - ); - expect(services.complete).toHaveBeenCalledWith( - runId, - leaseToken, - "turn-1", - expect.anything(), - new Date("2026-09-01T13:01:01.000Z") - ); + expect(services.complete).not.toHaveBeenCalled(); + expect(services.deferCompletion).not.toHaveBeenCalled(); }); it("ignores messages emitted before a tool call completes", async () => { diff --git a/tests/agent/hooks/session-owner.test.ts b/tests/agent/hooks/session-owner.test.ts index de2f7433..204df0d0 100644 --- a/tests/agent/hooks/session-owner.test.ts +++ b/tests/agent/hooks/session-owner.test.ts @@ -25,6 +25,9 @@ const scope = { userId: "user-1", workspaceId: "workspace-1" }; const context = { agent: { name: "test-agent" }, channel: { kind: "channel:linq" }, + cancel() { + throw new Error("Unexpected cancellation in fixture."); + }, async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, diff --git a/tests/agent/reply-targets.test.ts b/tests/agent/reply-targets.test.ts index 14e26726..bfc98bc9 100644 --- a/tests/agent/reply-targets.test.ts +++ b/tests/agent/reply-targets.test.ts @@ -68,23 +68,18 @@ describe("reply targets", () => { }); it("registers background workflow receipts through the public hook", async () => { - const handler = backgroundReplyTargetHook.events?.["action.result"]; + const handler = backgroundReplyTargetHook.events?.["task.started"]; await handler?.( { data: { - result: { - kind: "tool-result", - callId: "call-1", - toolName: "run_browser", - output: { status: "working", taskId: "task-from-hook" }, - }, - status: "completed", - sequence: 1, + callId: "call-1", + name: "run_browser", + kind: "tool", + taskId: "task-from-hook", turnId: "turn-1", - stepIndex: 0, }, meta: { at: "2026-09-03T12:00:00.000Z", id: "event-1" }, - type: "action.result", + type: "task.started", }, hookContext(linqAuth("hook-origin")) ); @@ -174,6 +169,9 @@ function hookContext(auth: SessionAuth): HookContext { return { agent: { name: "main" }, channel: { kind: "channel:linq" }, + cancel() { + throw new Error("Unexpected cancellation in fixture."); + }, async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, diff --git a/tests/agent/subagents/browser-agent/hooks/browser-trace-telemetry.test.ts b/tests/agent/subagents/browser-agent/hooks/browser-trace-telemetry.test.ts index 3d36a0b7..50c91dc5 100644 --- a/tests/agent/subagents/browser-agent/hooks/browser-trace-telemetry.test.ts +++ b/tests/agent/subagents/browser-agent/hooks/browser-trace-telemetry.test.ts @@ -40,6 +40,9 @@ const scope = { userId: "user-1", workspaceId: "workspace-1" }; const context = { agent: { name: "test-agent" }, channel: {}, + cancel() { + throw new Error("Unexpected cancellation in fixture."); + }, async getSandbox() { throw new Error("Sandbox access is outside this focused test."); }, diff --git a/tests/agent/tools/google-workspace.test.ts b/tests/agent/tools/google-workspace.test.ts index 1309550a..c9ac9839 100644 --- a/tests/agent/tools/google-workspace.test.ts +++ b/tests/agent/tools/google-workspace.test.ts @@ -35,6 +35,7 @@ describe("Google Workspace tools", () => { function toolContext() { return { + messages: [], abortSignal: new AbortController().signal, callId: "call-1", async getSandbox() { diff --git a/tests/agent/tools/run-browser.test.ts b/tests/agent/tools/run-browser.test.ts index 0d31962a..9ab29964 100644 --- a/tests/agent/tools/run-browser.test.ts +++ b/tests/agent/tools/run-browser.test.ts @@ -1,6 +1,7 @@ -import type { WorkflowToolContext } from "eve/tools"; +import type { AgentSession, WorkflowToolContext } from "eve/tools"; import { describe, expect, it, vi } from "vitest"; import runBrowser from "@agent/tools/run_browser"; +import { taskCompletionSchema } from "@agent/subagents/browser-agent/lib/completion"; import { toolContextFor } from "@tests/helpers/tool-context"; const completion = { @@ -10,57 +11,79 @@ const completion = { }; describe("structured browser workflow", () => { - it.each([undefined, "parked-worker"])( - "supplies the completion schema when agentId is %s", - async (agentId) => { - const agent = vi - .fn() - .mockResolvedValue(completion); - const context = { + it("reuses one child session and requests structured output for every continuation", async () => { + const send = vi.fn().mockResolvedValue({ + result: async () => ({ + data: completion, + status: "waiting", + message: undefined, + }), + }); + // SAFETY: This fixture only sends the fixed taskCompletionSchema and returns its corresponding output. + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Vitest erases the generic send result; this fixture exercises only the production completion schema. + const worker = { send } as AgentSession; + const agent = vi.fn().mockReturnValue(worker); + const receive = vi + .fn[0]>() + .mockResolvedValueOnce({ + input: { message: "Inspect the heading" }, + callId: "first", + abortSignal: new AbortController().signal, + }) + .mockResolvedValueOnce({ + input: { message: "Inspect the footer" }, + callId: "next", + abortSignal: new AbortController().signal, + }) + .mockRejectedValue(new Error("fixture finished")); + const reply = vi.fn[1]["reply"]>(); + await expect( + runBrowser.serve(receive, { ...toolContextFor(), agent, agents: {}, ask: vi.fn(), - }; - const result = await runBrowser.execute( - { message: "Inspect the heading", agentId }, - context - ); - - expect(result).toEqual(completion); - expect(agent).toHaveBeenCalledOnce(); - expect(agent.mock.calls[0]).toMatchObject([ - "browser-agent", - { - message: "Inspect the heading", - agentId, - outputSchema: { - type: "object", - required: ["images", "status", "message"], - properties: { - images: { type: "array", maxItems: 4 }, - status: { enum: ["success", "failure"] }, - }, - }, - }, - ]); - } - ); + reply, + }) + ).rejects.toThrow("fixture finished"); + expect(agent).toHaveBeenCalledExactlyOnceWith("browser-agent"); + expect(send).toHaveBeenCalledTimes(2); + expect(send.mock.calls[0]?.[0]).toBe("Inspect the heading"); + expect(send.mock.calls[0]?.[1]?.outputSchema).toBe(taskCompletionSchema); + expect(send.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal); + expect(send.mock.calls[1]?.[0]).toBe("Inspect the footer"); + expect(reply).toHaveBeenCalledTimes(2); + expect(reply).toHaveBeenLastCalledWith(completion); + }); it("rejects malformed completion instead of reporting success", async () => { - const agent = vi - .fn() - .mockResolvedValue({ status: "success", message: "Done" }); + const send = vi.fn().mockResolvedValue({ + result: async () => ({ + data: { status: "success", message: "Done" }, + status: "waiting", + message: undefined, + }), + }); + // SAFETY: This fixture deliberately returns invalid data to exercise runtime output validation. + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Vitest erases the generic send result; this fixture exercises only the production completion schema. + const worker = { send } as AgentSession; + const receive = vi + .fn[0]>() + .mockResolvedValue({ + input: { message: "Inspect the heading" }, + callId: "first", + abortSignal: new AbortController().signal, + }); + const reply = vi.fn[1]["reply"]>(); await expect( - runBrowser.execute( - { message: "Inspect the heading" }, - { - ...toolContextFor(), - agent, - agents: {}, - ask: vi.fn(), - } - ) + runBrowser.serve(receive, { + ...toolContextFor(), + agent: () => worker, + agents: {}, + ask: vi.fn(), + reply, + }) ).rejects.toThrow(/images/u); + expect(reply).not.toHaveBeenCalled(); }); }); diff --git a/tests/agent/tools/schedules.test.ts b/tests/agent/tools/schedules.test.ts index beef0b69..fa684004 100644 --- a/tests/agent/tools/schedules.test.ts +++ b/tests/agent/tools/schedules.test.ts @@ -349,6 +349,7 @@ function toolContext( conversationChannel: "eve" | "linq" = "linq" ) { return { + messages: [], abortSignal: new AbortController().signal, callId: "call-schedule", async getSandbox() { diff --git a/tests/helpers/tool-context.ts b/tests/helpers/tool-context.ts index 605d7fce..a1bccff1 100644 --- a/tests/helpers/tool-context.ts +++ b/tests/helpers/tool-context.ts @@ -24,6 +24,7 @@ export function toolContextFor({ } : undefined; return { + messages: [], abortSignal, callId, async getSandbox() { 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/runtime/fixture/agent/lib/model.ts b/tests/runtime/fixture/agent/lib/model.ts index 3be699c7..e99ef0bc 100644 --- a/tests/runtime/fixture/agent/lib/model.ts +++ b/tests/runtime/fixture/agent/lib/model.ts @@ -16,9 +16,21 @@ export function fixtureModel(worker: boolean) { .map((part) => (part.type === "text" ? part.text : "")) .join("") : ""; + const expectedTool = worker + ? "wait_for_cancellation" + : text.startsWith("Cancel:") + ? "task_cancel" + : "run_browser"; const returned = prompt .slice(userIndex + 1) - .some((message) => message.role === "tool"); + .some( + (message) => + message.role === "tool" && + message.content.some( + (part) => + part.type === "tool-result" && part.toolName === expectedTool + ) + ); let toolName: string | undefined; let input = "{}"; if (worker) { @@ -41,12 +53,12 @@ export function fixtureModel(worker: boolean) { }; input = JSON.stringify( text.startsWith("Resume:") - ? { ...assignment, agentId: text.slice(7) } + ? { ...assignment, taskId: text.slice(7) } : assignment ); } else if (!returned && text.startsWith("Cancel:")) { toolName = "task_cancel"; - input = JSON.stringify({ taskIds: [text.slice(7)] }); + input = JSON.stringify({ taskId: text.slice(7) }); } if (toolName && !tools?.some((tool) => tool.name === toolName)) throw new Error(`Missing fixture tool: ${toolName}`); diff --git a/tests/runtime/fixture/evals/workflow.eval.ts b/tests/runtime/fixture/evals/workflow.eval.ts index 5a97c88c..18d7c123 100644 --- a/tests/runtime/fixture/evals/workflow.eval.ts +++ b/tests/runtime/fixture/evals/workflow.eval.ts @@ -1,10 +1,9 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { setTimeout } from "node:timers/promises"; -import { defineEval, type EveEvalContext, type EveEvalTurn } from "eve/evals"; +import { defineEval, type EveEvalContext } from "eve/evals"; import { equals } from "eve/evals/expect"; import type { MessageStreamEvent } from "eve/client"; -import { browserTaskReceiptSchema } from "@agent/lib/browser-task"; import { taskCompletionOutputSchema } from "@agent/subagents/browser-agent/lib/completion"; async function waitForToolMarker( @@ -13,48 +12,29 @@ async function waitForToolMarker( signal: AbortSignal ) { const deadline = AbortSignal.any([signal, AbortSignal.timeout(10_000)]); - // The tool executes in Eve's worker process; its marker proves entry or abort observation. while (!existsSync(join(".eve", "fixture-tool", sessionId, marker))) { - // oxlint-disable-next-line eslint/no-await-in-loop -- bounded observation of a marker written by the runtime worker. + // oxlint-disable-next-line eslint/no-await-in-loop -- bounded observation of the runtime worker's cancellation marker. await setTimeout(10, undefined, { signal: deadline }); } } -async function followUntil( - t: EveEvalContext, - initial: EveEvalTurn, - predicate: (event: MessageStreamEvent) => boolean -) { - let turn = initial; - const events = [...turn.events]; - // Each iteration continues at the cursor of the preceding settled turn. - /* oxlint-disable eslint/no-await-in-loop */ - for (let attempt = 0; attempt < 8 && !events.some(predicate); attempt += 1) { - turn = await t.target - .watchTurn(turn.sessionId, { startIndex: turn.session.state.streamIndex }) - .result(); - turn.expectOk(); - events.push(...turn.events); - } - /* oxlint-enable eslint/no-await-in-loop */ - if (!events.some(predicate)) - throw new Error("Expected lifecycle event was not observed."); - return { turn, events }; -} - function calledAgent(events: readonly MessageStreamEvent[]) { - const called = events.find((event) => event.type === "subagent.called"); - if (!called?.data.agentId) - throw new Error("No child invocation was recorded."); - return called.data; + const opened = events.find((event) => event.type === "agent.started"); + if (!opened) throw new Error("No child session was recorded."); + return opened; } - function checkCompletion( t: EveEvalContext, events: readonly MessageStreamEvent[] ) { - const completed = events.find((event) => event.type === "subagent.completed"); - if (!completed) throw new Error("No child completion was recorded."); + const completed = events.findLast( + (event) => + event.type === "task.settled" && + event.data.name === "run_browser" && + event.data.status === "completed" + ); + if (completed?.type !== "task.settled") + throw new Error("No browser task completion was recorded."); t.check( taskCompletionOutputSchema.parse(completed.data.output), equals({ @@ -68,119 +48,73 @@ function checkCompletion( export default [ defineEval({ description: - "Completes background browser work and resumes the same structured worker", + "Completes browser work and resumes the same structured worker task", async test(t) { const first = await t.send("Start"); first.expectOk(); - first.requireToolCall("run_browser", { - status: "completed", - }); - const admission = first.events.find( - (candidate) => - candidate.type === "action.result" && - candidate.data.result.kind === "tool-result" && - candidate.data.result.toolName === "run_browser" - ); - if (admission?.type !== "action.result") - throw new Error("Browser admission event is missing."); - const parsed = browserTaskReceiptSchema.parse(admission.data.result); - t.check(parsed.output.status, equals("working")); - const initial = await followUntil( - t, - first, - (event) => event.type === "subagent.completed" + checkCompletion(t, first.events); + const opened = calledAgent(first.events); + if (!opened.data.taskId) + throw new Error("Browser task identity missing."); + const continued = await first.session.send( + `Resume:${opened.data.taskId}` ); - const called = calledAgent(initial.events); - checkCompletion(t, initial.events); - const resumed = await initial.turn.session.send( - `Resume:${String(called.agentId)}` + continued.expectOk(); + checkCompletion(t, continued.events); + const resumed = continued.events.find( + (event) => + event.type === "task.started" && event.data.name === "run_browser" ); - resumed.expectOk(); - const continued = await followUntil( - t, - resumed, - (event) => event.type === "subagent.completed" + if (resumed?.type !== "task.started") + throw new Error("Task continuation missing."); + t.check(resumed.data.taskId, equals(opened.data.taskId)); + t.check( + continued.events.some((event) => event.type === "agent.started"), + equals(false) ); - const next = calledAgent(continued.events); - t.check(next.agentId, equals(called.agentId)); - t.check(next.childSessionId, equals(called.childSessionId)); - checkCompletion(t, continued.events); }, }), defineEval({ - description: "Cancels admitted browser work while a child tool is running", + description: "Cancels running browser work and reuses its worker session", async test(t) { - const first = await t.send("Wait"); - first.expectOk(); - first.requireToolCall("run_browser", { - status: "completed", + const session = await t.session(); + const activity = await session.start("Wait"); + const opened = await activity.waitForEvent("agent.started"); + t.log("Opened browser session"); + if (!opened.data.taskId) + throw new Error("Browser task identity missing."); + await waitForToolMarker(opened.data.sessionId, "started", t.signal); + t.log("Browser tool running"); + const cancelled = await session.send(`Cancel:${opened.data.taskId}`, { + turnPolicy: "steer", }); - const admission = first.events.find( - (candidate) => - candidate.type === "action.result" && - candidate.data.result.kind === "tool-result" && - candidate.data.result.toolName === "run_browser" - ); - if (admission?.type !== "action.result") - throw new Error("Browser admission event is missing."); - const parsed = browserTaskReceiptSchema.parse(admission.data.result); - const activity = t.target.watchTurn(first.sessionId, { - startIndex: first.session.state.streamIndex, - }); - const called = - first.events.find((event) => event.type === "subagent.called") ?? - (await activity.waitForEvent("subagent.called")); - let toolStarted = false; - for await (const event of first.session.streamSubagent(called, { - signal: t.signal, - })) { - if ( - event.type === "actions.requested" && - event.data.actions.some( - (action) => - action.kind === "tool-call" && - action.toolName === "wait_for_cancellation" - ) - ) { - toolStarted = true; - break; - } - } - t.check(toolStarted, equals(true)); - await waitForToolMarker(called.data.childSessionId, "started", t.signal); - const cancelled = await first.session.send( - `Cancel:${parsed.output.taskId}` - ); + t.log("Cancellation turn settled"); cancelled.expectOk(); - - await activity.result(); + const first = await activity.result(); + t.log("Initial turn settled"); + first.expectOk(); cancelled.calledTool("task_cancel", { status: "completed", count: 1 }); - let childCancelled = false; - for await (const event of first.session.streamSubagent(called, { - signal: t.signal, - })) { - if (event.type === "turn.cancelled") { - childCancelled = true; - } - if (event.type === "session.waiting" && childCancelled) break; - if (event.type === "result.completed") - throw new Error("Cancelled worker produced a successful result."); - } - t.check(childCancelled, equals(true)); - await waitForToolMarker(called.data.childSessionId, "aborted", t.signal); + await waitForToolMarker(opened.data.sessionId, "aborted", t.signal); + const settlements = [...first.events, ...cancelled.events].filter( + (event) => event.type === "task.settled" + ); + t.check( + settlements.some( + (event) => + event.data.taskId === opened.data.taskId && + event.data.status === "cancelled" + ), + equals(true) + ); const resumed = await cancelled.session.send( - `Resume:${String(called.data.agentId)}` + `Resume:${opened.data.taskId}` ); resumed.expectOk(); - const continued = await followUntil( - t, - resumed, - (event) => event.type === "subagent.completed" + checkCompletion(t, resumed.events); + t.check( + resumed.events.some((event) => event.type === "agent.started"), + equals(false) ); - const next = calledAgent(continued.events); - t.check(next.agentId, equals(called.data.agentId)); - t.check(next.childSessionId, equals(called.data.childSessionId)); - checkCompletion(t, continued.events); }, }), ]; 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/tests/worker-input-bubbling.test.ts b/tests/worker-input-bubbling.test.ts index 9fd4e3aa..52e99a6c 100644 --- a/tests/worker-input-bubbling.test.ts +++ b/tests/worker-input-bubbling.test.ts @@ -57,7 +57,7 @@ describe("worker input bubbling", () => { "utf8" ); - expect(instructions).toContain("continue that worker with its `agentId`"); + expect(instructions).toContain("continue that worker with its `taskId`"); expect(instructions).toContain( "Before surfacing a `Needs user input:` blocker" ); 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_*"