---
title: "用 TypeScript 安全处理 AI 代理退款：签名、幂等与限额"
canonical: "https://siyuanailab.com/curation/translations/2b8224f6f6fa5ffa684a5da9b6f4c1b8-1b79928cc9e5c66ffa4ce75f3c55a00f98ca81668cbd6662b3a1d771aaa72fb7/"
dateModified: "2026-10-21"
datePublished: "2026-10-21"
---

[返回日报条目](https://siyuanailab.com/curation/#entry/2b8224f6f6fa5ffa684a5da9b6f4c1b8)

 

资料节译

 

# 用 TypeScript 安全处理 AI 代理退款：签名、幂等与限额

 

[阅读原文](https://dev.to/awab_alomran/handling-ai-agent-refunds-without-paying-twice-signatures-idempotency-and-limits-in-typescript-17ak)

 

现有资料无法确认原文完整，本页只翻译已取得的材料。

 

现有原件无法确认图片是否齐全。

 

AI 助手开始代表用户调用企业系统：“退掉那件蓝色夹克”“把我的预约改到周四”。读取订单无伤大雅。退款会转移资金，这才是有意思的工程问题。

 

以下是我会优先考虑的四种故障模式，以及我为每种情况使用的 TypeScript。示例使用 [`allowfy`](https://www.npmjs.com/package/allowfy)，这是一个小型开源软件包（零依赖，适用于 Web `Request`/`Response`），但这些模式适用于任何基于 webhook 的集成。

 

你的退款端点会暴露在互联网上。每个请求都必须证明它来自你的网关，而且是近期发出的。

 

方案：`Allowfy-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`。在解析任何 JSON 之前，针对**原始**请求体进行验证，使用恒定时间比较和时间窗口：

 

```
import { verifySignature } from "allowfy";
const raw = await request.text(); // raw, not request.json()
const ok = await verifySignature(process.env.ALLOWFY_WEBHOOK_SECRET!, raw, request.headers.get("allowfy-signature"));
if (!ok) return new Response("bad signature", { status: 401 });
```

 

签名载荷中的时间戳可以阻止重放：攻击者即使截获了有效请求，也无法在十分钟后再次发送它。

 

不要让智能体的请求直接触发退款。把流程拆开：

 

-

 

**报价**：计算出精确条款（这个订单、这个金额、这种方式）。不要做任何改动。

 

- 客户批准**这些确切条款**；你的规则决定是否还需要团队审核；
 
-

 

**执行**：按照已批准的条款办理。

 

```
import { createHandler, refuse } from "allowfy";
export const POST = createHandler({
  secret: process.env.ALLOWFY_WEBHOOK_SECRET!,
  quote: {
    async "orders.return"({ order_id }, { customerRef }) {
      const o = await db.order(order_id, customerRef);
      if (!o) refuse("not_found", "Order not found", 404);
      if (o.status !== "paid") refuse("not_returnable", "This order can't be returned");
      return {
        terms: { order_id: o.id, refund_minor: o.totalMinor, currency: o.currency },
        summary: `Return ${o.item} for a ${o.totalMinor / 100} ${o.currency} refund`,
        amountMinor: o.totalMinor, // lets amount rules apply
        currency: o.currency,
      };
    },
  },
  // execute: below
});
```

 

如果批准后条款发生变化（例如价格更新、订单不同），原批准便不再适用，需要重新批准。该规则由网关负责执行，而你的 `quote` 会把条款明确下来。

 

典型情况：你的系统发出了退款，随后连接中断，网关还没收到回复。简单重试会导致重复退款。

 

两条规则可以解决这个问题：

 

- 每次执行都带有一个稳定的**操作 ID**（也会作为`Idempotency-Key`发送）。每个 ID 的操作**最多执行一次**，重试时返回相同结果。
 
- 任何不明确的答复（5xx、超时）都意味着**结果未知**，绝不能说“失败”。网关会使用同一 ID 重试，最后一次尝试后由人工处理。

 

```
execute: {
  async "orders.return"(terms, { operationId }) {
    // A unique index on returns.operation_id turns a retry into a no-op.
    const inserted = await db.returns.insertIfAbsent({ operation_id: operationId, order_id: terms.order_id, refund_minor: terms.refund_minor });
    if (inserted) await payments.refund(terms.order_id, terms.refund_minor, { idempotencyKey: operationId });
    return { summary: `Order ${terms.order_id} returned; refund on its way` };
  },
},
```

 

还要注意传递给支付服务提供商的幂等键：这一保证必须贯穿整个调用链。

 

`refuse()`用于有意给出“否”的答复（已在店内退货）：没有任何变更，客户会看到你的消息。其他任何抛出的错误都会变成 500，这意味着“结果未知，请重试”。

 

按单次请求设置限额（“退款金额不超过 100 美元时，只需客户批准”）很容易绕过。限额需要按时间计算，并按客户统计，涵盖所有智能体：

 

```
{
  "action": "orders.return",
  "decision": "user_approval",
  "conditions": { "maxAmountMinor": 10000, "maxTotalMinor": 15000, "maxCount": 3, "windowDays": 7 },
  "otherwise": "business_review"
}
```

 

有两个实现细节很重要：

 

-

 

## **跨智能体计数。**切换助手不能重置计数器。

 

**按客户串行处理。**两个同时到达的请求不能都钻过限额。在 Postgres 中，使用以客户为键、作用域限定在事务内的 advisory lock 即可：`SELECT pg_advisory_xact_lock(hashtextextended('limit:' || $1, 0))`。

 

在 Supabase（Deno）上，整个处理程序只需一次调用：

 

```
import { createHandler } from "npm:allowfy";
Deno.serve(createHandler({ secret: Deno.env.get("ALLOWFY_WEBHOOK_SECRET")!, authenticate, read, quote, execute }));
```

 

在 Express 中，应在任何 JSON 请求体解析器之前挂载它（签名覆盖原始请求体）：

 

```
import { createHandler, toNodeListener } from "allowfy";
app.post("/allowfy", toNodeListener(createHandler({ secret, authenticate, read, quote, execute })));
```

 

- 在原始请求体上验证签名，并设置时间窗口
 
- 报价和执行分开；报价不做任何改动
 
- 执行按操作 ID 保证幂等，直到支付服务提供商这一层
 
- 绝不把未知结果报告为失败
 
- 按客户跨时间和智能体设置限额，并串行处理
 
- 每个步骤都记录到某个可供导出的位置

 

关于网关这一侧（带签名密钥的智能体身份、DPoP 绑定令牌、客户登录、规则、审批、签名账本、紧急停止），我正在通过 [Allowfy](https://allowfy.com/) 构建。本文较长、非代码的版本发布在 [Allowfy 博客](https://allowfy.com/blog/let-ai-agents-handle-refunds-safely)。如果你想看看 AI 智能体如今如何看待你自己的域名，[Allowfy Check](https://check.allowfy.com/) 免费且开源。

 

有问题，或者有我遗漏的故障模式吗？欢迎在评论中告诉我。

 

AI 翻译；具体表述请与原文对照。
