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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pnpm-lock.yaml
*.md
.next/
.eve/
.output/
.vercel/
coverage/
dist/
next-env.d.ts
3 changes: 3 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 100
}
35 changes: 25 additions & 10 deletions agent/channels/linq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ export function isAllowedBellMessage(message: Pick<LinqMessage, "author" | "raw"
if (message.author.isBot) return false;
const incoming = inboundMessage.safeParse(message.raw);
const receivingNumber = phoneNumber.safeParse(process.env.BELL_LINQ_PHONE_NUMBER?.trim());
const allowedSenders = z.array(phoneNumber).nonempty().safeParse(
(process.env.BELL_LINQ_ALLOWED_SENDERS ?? "").split(",").map((value) => value.trim()),
);
const allowedSenders = z
.array(phoneNumber)
.nonempty()
.safeParse(
(process.env.BELL_LINQ_ALLOWED_SENDERS ?? "").split(",").map((value) => value.trim()),
);

return incoming.success && receivingNumber.success && allowedSenders.success
&& incoming.data.chat.owner_handle.handle === receivingNumber.data
&& allowedSenders.data.includes(incoming.data.sender_handle.handle);
return (
incoming.success &&
receivingNumber.success &&
allowedSenders.success &&
incoming.data.chat.owner_handle.handle === receivingNumber.data &&
allowedSenders.data.includes(incoming.data.sender_handle.handle)
);
}

export default linqChannel({
Expand All @@ -41,11 +48,19 @@ export default linqChannel({
const auth = defaultLinqAuth(message);
const incoming = inboundMessage.parse(message.raw);
return {
auth: { ...auth, attributes: { ...auth.attributes, bell_watch_chat: incoming.chat.id, bell_watch_sender: incoming.sender_handle.handle, bell_watch_line: incoming.chat.owner_handle.handle } },
auth: {
...auth,
attributes: {
...auth.attributes,
bell_watch_chat: incoming.chat.id,
bell_watch_sender: incoming.sender_handle.handle,
bell_watch_line: incoming.chat.owner_handle.handle,
},
},
context: [
"This request arrived through Bell's private Linq iMessage or SMS channel. "
+ "Reply concisely in this chat. Use the same browser for follow-up requests. "
+ "For a human-only step, send the human_handoff tool's link and wait for a reply.",
"This request arrived through Bell's private Linq iMessage or SMS channel. " +
"Reply concisely in this chat. Use the same browser for follow-up requests. " +
"For a human-only step, send the human_handoff tool's link and wait for a reply.",
],
};
},
Expand Down
125 changes: 81 additions & 44 deletions agent/extensions/link/lib/spend-requests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { z } from "zod";
import { isLinkSandbox, isSandboxRequest } from "../../../../server/link/sandbox";

export const spendRequestId = z.string().min(1).max(255)
export const spendRequestId = z
.string()
.min(1)
.max(255)
.regex(/^[A-Za-z0-9_-]+$/, "Invalid Link spend-request ID.");
export const spendRequestInput = z.strictObject({ id: spendRequestId });

Expand All @@ -17,9 +20,23 @@ export async function linkMetadataRequest<T>(request: () => T | Promise<T>): Pro
} catch (error) {
// The SDK includes provider response bodies in its errors. Do not persist
// those bodies, while preserving Eve's separate authorization pause signals.
if (error instanceof Error && ["LinkApiError", "LinkResponseError", "LinkTransportError", "LinkConfigurationError"].includes(error.name)) {
const status = "status" in error && typeof error.status === "number" && Number.isInteger(error.status)
&& error.status >= 100 && error.status <= 599 ? ` (HTTP ${error.status})` : "";
if (
error instanceof Error &&
[
"LinkApiError",
"LinkResponseError",
"LinkTransportError",
"LinkConfigurationError",
].includes(error.name)
) {
const status =
"status" in error &&
typeof error.status === "number" &&
Number.isInteger(error.status) &&
error.status >= 100 &&
error.status <= 599
? ` (HTTP ${error.status})`
: "";
throw new Error(`Link request failed${status}. Check the existing request before retrying.`);
}
throw error;
Expand All @@ -42,51 +59,71 @@ const metadata = z.object({
merchant_name: z.string().optional(),
merchant_url: z.string().optional(),
context: z.string().optional(),
line_items: z.array(z.object({
name: z.string(),
url: z.string().optional(),
image_url: z.string().optional(),
description: z.string().optional(),
sku: z.string().optional(),
totals: z.array(total).optional(),
quantity: z.number().optional(),
unit_amount: z.number().optional(),
product_url: z.string().optional(),
})).optional(),
line_items: z
.array(
z.object({
name: z.string(),
url: z.string().optional(),
image_url: z.string().optional(),
description: z.string().optional(),
sku: z.string().optional(),
totals: z.array(total).optional(),
quantity: z.number().optional(),
unit_amount: z.number().optional(),
product_url: z.string().optional(),
}),
)
.optional(),
totals: z.array(total).optional(),
approval_url: z.string().optional(),
activity_url: z.string().optional(),
expires_at: z.number().optional(),
recurring: z.object({
interval: z.string(),
interval_count: z.number().optional(),
}).nullable().optional(),
payment_status_details: z.object({
outcome: z.enum(["success", "failure"]),
code: z.string().nullable().optional(),
decline_code: z.string().nullable().optional(),
amount: z.number(),
currency: z.string(),
created: z.number().nullable().optional(),
refund_details: z.object({
recurring: z
.object({
interval: z.string(),
interval_count: z.number().optional(),
})
.nullable()
.optional(),
payment_status_details: z
.object({
outcome: z.enum(["success", "failure"]),
code: z.string().nullable().optional(),
decline_code: z.string().nullable().optional(),
amount: z.number(),
currency: z.string(),
state: z.string(),
created: z.number(),
}).nullable().optional(),
}).nullable().optional(),
status_details: z.object({
requires_action: z.object({
failure_code: z.string().optional(),
next_action: z.object({
type: z.string().optional(),
display_message: z.string().optional(),
action_url: z.string().nullable().optional(),
resolution: z.string().optional(),
expires_at: z.number().nullable().optional(),
}).optional(),
}).optional(),
}).nullable().optional(),
created: z.number().nullable().optional(),
refund_details: z
.object({
amount: z.number(),
currency: z.string(),
state: z.string(),
created: z.number(),
})
.nullable()
.optional(),
})
.nullable()
.optional(),
status_details: z
.object({
requires_action: z
.object({
failure_code: z.string().optional(),
next_action: z
.object({
type: z.string().optional(),
display_message: z.string().optional(),
action_url: z.string().nullable().optional(),
resolution: z.string().optional(),
expires_at: z.number().nullable().optional(),
})
.optional(),
})
.optional(),
})
.nullable()
.optional(),
created_at: z.string(),
updated_at: z.string(),
});
Expand All @@ -99,7 +136,7 @@ export function spendRequestMetadata(value: unknown) {

export function spendRequestListMetadata(value: unknown) {
if (!Array.isArray(value)) throw new Error("Unexpected Link spend-request list.");
return value.map(spendRequestMetadata).filter(item => isSandboxRequest(item.id));
return value.map(spendRequestMetadata).filter((item) => isSandboxRequest(item.id));
}

export function spendApprovalMetadata(value: unknown) {
Expand Down
12 changes: 10 additions & 2 deletions agent/extensions/link/tools/cancel_spend_request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { cancel_spend_request } from "@stripe/link-integrations-eve/tools";
import { defineTool } from "eve/tools";
import { freshLinkContext } from "../../../lib/link-auth";
import { linkMetadataRequest, spendRequestInput, spendRequestMetadata } from "../lib/spend-requests";
import {
linkMetadataRequest,
spendRequestInput,
spendRequestMetadata,
} from "../lib/spend-requests";

export default defineTool({
description: cancel_spend_request.description,
Expand All @@ -10,6 +14,10 @@ export default defineTool({
async execute(args, ctx) {
const input = spendRequestInput.parse(args);
ctx.abortSignal.throwIfAborted();
return spendRequestMetadata(await linkMetadataRequest(() => cancel_spend_request.execute(input, freshLinkContext(ctx, input.id))));
return spendRequestMetadata(
await linkMetadataRequest(() =>
cancel_spend_request.execute(input, freshLinkContext(ctx, input.id)),
),
);
},
});
39 changes: 31 additions & 8 deletions agent/extensions/link/tools/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,43 @@ import { isLinkSandbox } from "../../../../server/link/sandbox";
import { linkMetadataRequest } from "../lib/spend-requests";

export default defineTool({
description: "Submit an already-approved Link purchase in the existing browser. Supply exact CSS payment-field mappings and the approved amount/currency. Virtual-card values stay server-side. Shared Payment Tokens and Link Pay Tokens require a separate integration and are not accepted. Use only once per purchase; an uncertain outcome requires checking the merchant and Link before any retry. Human handoff remains available for 3DS.",
description:
"Submit an already-approved Link purchase in the existing browser. Supply exact CSS payment-field mappings and the approved amount/currency. Virtual-card values stay server-side. Shared Payment Tokens and Link Pay Tokens require a separate integration and are not accepted. Use only once per purchase; an uncertain outcome requires checking the merchant and Link before any retry. Human handoff remains available for 3DS.",
inputSchema: checkoutInput,
async execute(args, ctx) {
const input = checkoutInput.parse(args);
linkPrincipal(ctx);
ctx.abortSignal.throwIfAborted();
const retrieve = (include?: [typeof input.credential]) => linkMetadataRequest(() => retrieve_spend_request.execute({ id: input.id, ...(include ? { include } : {}) }, freshLinkContext(ctx, input.id)));
const retrieve = (include?: [typeof input.credential]) =>
linkMetadataRequest(() =>
retrieve_spend_request.execute(
{ id: input.id, ...(include ? { include } : {}) },
freshLinkContext(ctx, input.id),
),
);
const origins = paymentOrigins(await retrieve(), input);
const output = await runCancelableBrowserWork(ctx.session.id, ctx.abortSignal, () => runBrowserOperation(ctx.session.id, browser => browser.withHostedPage(ctx.abortSignal, (page, connection) =>
checkoutInHostedPage(page, connection, input, origins, async () => {
const result = await retrieve([input.credential]);
if (JSON.stringify(paymentOrigins(result, input)) !== JSON.stringify(origins)) throw new Error("Link approval changed.");
return result;
}, ctx.abortSignal), true)));
const output = await runCancelableBrowserWork(ctx.session.id, ctx.abortSignal, () =>
runBrowserOperation(ctx.session.id, (browser) =>
browser.withHostedPage(
ctx.abortSignal,
(page, connection) =>
checkoutInHostedPage(
page,
connection,
input,
origins,
async () => {
const result = await retrieve([input.credential]);
if (JSON.stringify(paymentOrigins(result, input)) !== JSON.stringify(origins))
throw new Error("Link approval changed.");
return result;
},
ctx.abortSignal,
),
true,
),
),
);
return isLinkSandbox() ? { ...output.result, test: true } : output.result;
},
});
6 changes: 4 additions & 2 deletions agent/extensions/link/tools/create_report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { create_report } from "@stripe/link-integrations-eve/tools";
import { defineTool } from "eve/tools";
import { freshLinkContext } from "../../../lib/link-auth";

export default defineTool({ ...create_report,
execute: (input, ctx) => create_report.execute(input, freshLinkContext(ctx, input.spend_request_id)),
export default defineTool({
...create_report,
execute: (input, ctx) =>
create_report.execute(input, freshLinkContext(ctx, input.spend_request_id)),
});
9 changes: 8 additions & 1 deletion agent/extensions/link/tools/create_spend_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ export default defineTool({
approval: create_spend_request.approval,
async execute(input, ctx) {
ctx.abortSignal.throwIfAborted();
const result = spendRequestMetadata(await linkMetadataRequest(() => create_spend_request.execute(sandboxPurchase(input), freshLinkContext(ctx, undefined, "create"))));
const result = spendRequestMetadata(
await linkMetadataRequest(() =>
create_spend_request.execute(
sandboxPurchase(input),
freshLinkContext(ctx, undefined, "create"),
),
),
);
rememberSandboxRequest(result.id);
return result;
},
Expand Down
3 changes: 2 additions & 1 deletion agent/extensions/link/tools/list_payment_methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { list_payment_methods } from "@stripe/link-integrations-eve/tools";
import { defineTool } from "eve/tools";
import { freshLinkContext } from "../../../lib/link-auth";

export default defineTool({ ...list_payment_methods,
export default defineTool({
...list_payment_methods,
execute: (input, ctx) => list_payment_methods.execute(input, freshLinkContext(ctx)),
});
3 changes: 2 additions & 1 deletion agent/extensions/link/tools/list_shipping_addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { list_shipping_addresses } from "@stripe/link-integrations-eve/tools";
import { defineTool } from "eve/tools";
import { freshLinkContext } from "../../../lib/link-auth";

export default defineTool({ ...list_shipping_addresses,
export default defineTool({
...list_shipping_addresses,
execute: (input, ctx) => list_shipping_addresses.execute(input, freshLinkContext(ctx)),
});
6 changes: 5 additions & 1 deletion agent/extensions/link/tools/list_spend_requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default defineTool({
approval: list_spend_requests.approval,
async execute(input, ctx) {
ctx.abortSignal.throwIfAborted();
return spendRequestListMetadata(await linkMetadataRequest(() => list_spend_requests.execute(input, freshLinkContext(ctx, undefined, "list"))));
return spendRequestListMetadata(
await linkMetadataRequest(() =>
list_spend_requests.execute(input, freshLinkContext(ctx, undefined, "list")),
),
);
},
});
12 changes: 10 additions & 2 deletions agent/extensions/link/tools/request_spend_approval.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { request_spend_approval } from "@stripe/link-integrations-eve/tools";
import { defineTool } from "eve/tools";
import { freshLinkContext } from "../../../lib/link-auth";
import { linkMetadataRequest, spendApprovalMetadata, spendRequestInput } from "../lib/spend-requests";
import {
linkMetadataRequest,
spendApprovalMetadata,
spendRequestInput,
} from "../lib/spend-requests";

export default defineTool({
description: request_spend_approval.description,
Expand All @@ -10,6 +14,10 @@ export default defineTool({
async execute(args, ctx) {
const input = spendRequestInput.parse(args);
ctx.abortSignal.throwIfAborted();
return spendApprovalMetadata(await linkMetadataRequest(() => request_spend_approval.execute(input, freshLinkContext(ctx, input.id))));
return spendApprovalMetadata(
await linkMetadataRequest(() =>
request_spend_approval.execute(input, freshLinkContext(ctx, input.id)),
),
);
},
});
Loading
Loading