npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wecode-team/payment-paypal

v0.0.1

Published

We0 PayPal SDK —— 自定义订单支付,服务端下单与验签

Readme

@wecode-team/payment-paypal 集成指南

当用户选择「PayPal 支付」功能时,在当前 Razzle / Express 项目中集成 @wecode-team/payment-paypal SDK。

SDK 自动从 Supabase 配置表读取 PayPal 凭证,业务侧只需传 supabase 实例。

1. 安装依赖

npm install @wecode-team/payment-paypal

package.json 的 dependencies 中确保:

"@wecode-team/payment-paypal": "latest"

运行环境:Node.js 18+

2. 核心参数

2.1 createPayPalClient — 创建客户端

唯一必填参数supabase 实例。

const client = await createPayPalClient({ supabase });

强约束(必须遵守)

  • 只传 supabase,禁止传 config 参数覆盖读取规则。配置读取策略由 SDK 内部管理(设置 PROJECT_ID 时自动按 CMS 配置表约定读取,否则用通用 config 表)。
  • supabase 必须使用 service role key 创建,不能用 anon key(anon key 无法读取配置表)。
  • 禁止手动拼接 PayPal client_id / client_secret / webhook_id,全部由 SDK 自动管理。

正确写法

// ✅ 正确:只传 supabase,配置由 SDK 内部读取
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const client = await createPayPalClient({ supabase });

错误写法

// ❌ 错误:传 config 覆盖读取规则
const client = await createPayPalClient({
  supabase,
  config: { clientIdField: "MY_PAYPAL_ID" }, // 禁止手动覆盖
});

// ❌ 错误:用 anon key
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!, // anon key 无法读取配置表
);

配置中心字段:SDK 从 Supabase 配置表只读取三个字段(CMS 模式字段名见括号):

| 字段 | CMS 字段名 | 必填 | 说明 | |------|-----------|------|------| | client_id | PAYPAL_CLIENT_ID | 是 | PayPal 应用 Client ID | | client_secret | PAYPAL_CLIENT_SECRET | 是 | PayPal 应用 Client Secret | | webhook_id | PAYPAL_WEBHOOK_ID | 否 | 仅 verifyWebhook() 需要 |

运行环境(Sandbox / Production):不在配置表里,由环境变量 PAYPAL_ENVIRONMENT 决定:

  • PAYPAL_ENVIRONMENT=sandbox → 走 PayPal 沙盒
  • PAYPAL_ENVIRONMENT=live(或 production)→ 走 PayPal 生产
  • 未设置时默认 live,避免生产凭证误打到 sandbox。本地联调请显式设置 PAYPAL_ENVIRONMENT=sandbox

2.2 createOrder — 创建支付订单

type PayPalCreateOrderInput = {
  amount: number;        // 金额,单位为**最小货币单位**(美分 = 1/100 USD)
  currency: "USD" | "CNY"; // 货币代码
  orderId: string;       // 业务订单 ID,必须唯一
  description: string;   // 支付描述,用户可在 PayPal 页面看到
  returnUrl: string;     // 审批成功回跳地址
  cancelUrl: string;     // 审批取消回跳地址
  brandName?: string;    // PayPal 审批页展示的品牌名
  locale?: string;       // 审批页语言,如 "en-US"
};

type PayPalCreateOrderResult = {
  paypalOrderId: string; // PayPal Order ID,捕获时需要
  orderId: string;       // 业务订单 ID(回传)
  approveUrl: string;    // 前端跳转地址
  status: string;        // PayPal 订单状态
};

强约束

  • amount 单位是最小货币单位:USD 传美分($19.99 → 1999),CNY 传分(¥19.99 → 1999)。
  • orderId 必须全局唯一,建议格式:order_${userId}_${timestamp}
  • returnUrlcancelUrl 必须是完整的绝对 URL,不能是相对路径。
  • 业务侧需保存 paypalOrderIdorderId 的对应关系,回跳捕获时要用。

2.3 captureOrder — 捕获已审批订单

用户在 PayPal 审批后回跳到 returnUrl,服务端拿 paypalOrderId 捕获款项。这一步是收款关键,缺失则资金不会真正到账。

type PayPalCaptureOrderInput = {
  paypalOrderId: string; // createOrder 返回的 PayPal Order ID
};

type PayPalCaptureOrderResult = {
  captureId: string;     // PayPal 捕获 ID
  paypalOrderId: string; // PayPal Order ID(回传)
  orderId: string;       // 业务订单 ID
  status: "success" | "failed" | "pending";
  amount: number;        // 最小货币单位(美分 / 分)
  currency: "USD" | "CNY";
  raw: unknown;          // PayPal 原始返回
};

强约束

  • captureOrder() 只能在服务端调用,不要让前端直接持有凭证去捕获。
  • 回跳地址 returnUrl 上的 token 查询参数即为 paypalOrderId,前端把它发回服务端捕获。
  • status === "success" 作为入账依据,不要只凭"用户回跳成功"就认为已支付。

2.4 verifyWebhook — 验签

type PayPalWebhookEvent = {
  eventType: string;     // PayPal 事件类型
  captureId?: string;
  paypalOrderId: string;
  orderId: string;
  status: "success" | "failed" | "pending";
  amount: number;
  currency?: "USD" | "CNY";
  raw: unknown;
};

强约束(必须遵守)

  • body 必须是原始请求体字符串,不能先 JSON.parse() 再传入。
  • Webhook 路由必须使用 express.raw({ type: "application/json" }) 中间件。
  • headers 必须保留 PayPal 原始请求头(paypal-transmission-idpaypal-transmission-timepaypal-cert-urlpaypal-auth-algopaypal-transmission-sig),直接透传 req.headers

事件映射:PAYMENT.CAPTURE.COMPLETED"success"PAYMENT.CAPTURE.DENIED / PAYMENT.CAPTURE.DECLINED"failed"CHECKOUT.ORDER.APPROVED"pending"

正确写法

// ✅ 正确:express.raw 中间件 + 原始 body + 透传 headers
router.post(
  "/api/payment/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const event = await client.verifyWebhook(
      req.headers,
      req.body.toString("utf8"), // 原始 buffer 转 string
    );
  },
);

错误写法

// ❌ 错误:先 JSON.parse 再传入
router.post("/api/payment/webhook", async (req, res) => {
  const body = JSON.parse(req.body); // 已经丢失原始格式,验签必然失败
  const event = await client.verifyWebhook(req.headers, body);
});

// ❌ 错误:使用 express.json() 中间件
router.post(
  "/api/payment/webhook",
  express.json(), // json() 会自动 parse,body 已不是原始格式
  async (req, res) => { ... },
);

2.5 webhook_id 配置(无自动注册)

PayPal Server SDK 当前不支持 webhook 管理接口,本包不提供自动注册。需要:

  1. 在 PayPal 后台手动创建 webhook,回调地址指向 /api/payment/webhook
  2. 把生成的 webhook_id 写入 Supabase 配置中心(默认字段 webhook_id,CMS 模式字段 PAYPAL_WEBHOOK_ID)。

说明webhook_id 只在调用 verifyWebhook() 时需要;若当前需求不验签可暂不配置。

2.6 其他 API

| 方法 | 说明 | |------|------| | client.isSandbox | 当前是否为沙盒环境(environment === "sandbox") | | initPayPal(options) / paypal | 初始化默认单例并复用 | | verifyPayPalWebhook(headers, body, options) | 低级验签函数,适用于已有自己凭证来源的场景,从 @wecode-team/payment-paypal/webhook 导入 |

3. 默认落地路径

本项目使用 Razzle / Express,不要按 Next.js App Router 理解

AI 必须生成以下三个接口,不需要等用户定义:

3.1 POST /api/payment/create-paypal-order

职责:接收前端请求,创建 PayPal Order,返回审批跳转 URL。

放置位置src/routes/payment.tssrc/api/payment.ts(根据项目现有路由结构调整)。

import express from "express";
import { createClient } from "@supabase/supabase-js";
import { createPayPalClient } from "@wecode-team/payment-paypal";

const router = express.Router();

router.post("/api/payment/create-paypal-order", async (req, res) => {
  try {
    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!,
    );
    const client = await createPayPalClient({ supabase });

    const order = await client.createOrder({
      amount: req.body.amount,
      currency: req.body.currency,
      orderId: req.body.orderId,
      description: req.body.description,
      returnUrl: req.body.returnUrl,
      cancelUrl: req.body.cancelUrl,
      brandName: req.body.brandName,
      locale: req.body.locale,
    });

    res.json({ code: 200, message: "success", data: order });
  } catch (error) {
    const message = (error as Error).message;
    console.error("[payment] create-paypal-order failed:", message);
    res.status(500).json({ code: 500, message });
  }
});

3.2 POST /api/payment/capture-paypal-order

职责:用户审批回跳后,捕获已审批的 PayPal Order,确认收款。

放置位置:同上,与 create 路由放在同一文件。

router.post("/api/payment/capture-paypal-order", async (req, res) => {
  try {
    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!,
    );
    const client = await createPayPalClient({ supabase });

    const capture = await client.captureOrder({
      paypalOrderId: req.body.paypalOrderId,
    });

    console.log("[payment] capture:", capture.status, capture.orderId);

    if (capture.status === "success") {
      // TODO: 标记订单支付成功,更新数据库
      // await db.update(orders).set({ status: "paid" }).where(eq(orders.id, capture.orderId));
    }

    res.json({ code: 200, message: "success", data: capture });
  } catch (error) {
    const message = (error as Error).message;
    console.error("[payment] capture-paypal-order failed:", message);
    res.status(500).json({ code: 500, message });
  }
});

3.3 POST /api/payment/webhook

职责:接收 PayPal webhook 回调,验签,处理支付结果(兜底,防止前端未触发捕获)。

放置位置:同上,与其他支付路由放在同一文件。

router.post(
  "/api/payment/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      const supabase = createClient(
        process.env.SUPABASE_URL!,
        process.env.SUPABASE_SERVICE_ROLE_KEY!,
      );
      const client = await createPayPalClient({ supabase });

      const event = await client.verifyWebhook(
        req.headers,
        req.body.toString("utf8"),
      );

      console.log("[payment] webhook received:", event.status, event.orderId);

      if (event.status === "success") {
        // TODO: 标记订单支付成功,更新数据库
      } else if (event.status === "failed") {
        // TODO: 标记订单支付失败
      }

      res.json({ received: true });
    } catch (error) {
      const message = (error as Error).message;
      console.error("[payment] webhook verification failed:", message);
      res.status(400).json({ code: 400, message });
    }
  },
);

3.4 前端跳转与回跳捕获

// 1. 创建 order,跳转到 PayPal 审批页
const res = await fetch("/api/payment/create-paypal-order", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: 1999,
    currency: "USD",
    orderId: `order_${userId}_${Date.now()}`,
    description: "Pro plan",
    returnUrl: `${window.location.origin}/payment/result?status=success`,
    cancelUrl: `${window.location.origin}/payment/result?status=cancel`,
  }),
});

if (!res.ok) {
  const error = await res.json().catch(() => ({ message: "网络错误" }));
  toast.error(error.message || "支付创建失败,请稍后重试");
  return;
}

const result = await res.json();
window.location.assign(result.data.approveUrl);

// 2. 用户审批后回跳 returnUrl,PayPal 会带上 token(即 paypalOrderId)
//    前端把它发回服务端完成捕获
const params = new URLSearchParams(window.location.search);
const paypalOrderId = params.get("token");
if (paypalOrderId) {
  await fetch("/api/payment/capture-paypal-order", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ paypalOrderId }),
  });
}

4. 安全边界

强约束(必须遵守)

| # | 约束 | 违反后果 | |---|------|----------| | 1 | 禁止将 client_secret / access token / webhook_id 暴露给前端 | 凭证泄露,资金风险 | | 2 | Webhook 路由必须保留原始 body,不能先 JSON.parse | 验签必然失败 | | 3 | amount 参数必须在服务端校验范围(如最低 $1.00) | 防止前端篡改金额 | | 4 | 以服务端 captureOrder / webhook 的 success 为入账依据 | 防止仅凭回跳就误判已支付 | | 5 | 禁止在前端硬编码 PayPal 凭证 | 凭证泄露 |

5. 错误处理

5.1 SDK 层错误

| 场景 | 错误信息 | 处理方式 | |------|----------|----------| | Supabase 连接失败 | Failed to load config | 检查 SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY 环境变量 | | 配置表不存在 / 无记录 | No config row found | 确认 Supabase 配置表已创建且包含 PayPal 凭证 | | 凭证为空 | "client_id" is empty / "client_secret" is empty | 在配置表中补全 client_id / client_secret | | webhook_id 缺失 | webhook_id not found in config | 在 PayPal 后台创建 webhook 并把 webhook_id 写入配置表 | | Webhook 验签失败 | PayPal webhook verification failed | 确认 body 是原始格式,确认 webhook_id 与回调来源一致 |

5.2 前端错误处理

前端必须处理以下场景:

// 1. 网络错误
if (!res.ok) {
  const error = await res.json().catch(() => ({ message: "网络错误" }));
  toast.error(error.message || "支付创建失败,请稍后重试");
  return;
}

// 2. 跳转失败(PayPal 页面加载失败)
window.location.assign(result.data.approveUrl);
// 无需额外处理,浏览器会显示 PayPal 的错误页面

// 3. 回调页面
// success 页面:发起捕获 → 展示支付成功 + 订单详情
// cancel 页面:展示支付取消 + 重新支付按钮

6. 平台生成场景职责边界

| 角色 | 职责 | |------|------| | 平台层 | 在 PayPal 后台创建 webhook,把 webhook_id 写入配置中心 | | AI | 注册支付相关 route,调用 SDK | | SDK | 读取凭证、创建 order、捕获 order、验签 | | 用户 | 不需要在代码里手填任何凭证 |

默认路径是"服务端创建 order + 回跳后服务端捕获 + webhook 兜底",而不是让前端直接对接 PayPal 凭证。

7. 规则摘要

  1. 唯一必填参数createPayPalClient({ supabase }),禁止传 config 覆盖读取规则。
  2. 三个默认接口POST /api/payment/create-paypal-order + POST /api/payment/capture-paypal-order + POST /api/payment/webhook,AI 直接生成,不需要等用户定义。
  3. 必须捕获:用户审批回跳后服务端调用 captureOrder,以 success 为入账依据。
  4. Webhook 原始 body:必须用 express.raw() 中间件,透传 req.headers,不能先 JSON.parse()
  5. 前端只做跳转:发请求 → 拿 data.approveUrlwindow.location.assign(),回跳后把 token 发回捕获。
  6. 不要发明抽象层:禁止生成 PaymentServicePayPalManagerPaymentProvider 等,直接 route → SDK。
  7. webhook_id 手动配置:PayPal SDK 不支持自动注册,需在后台创建 webhook 并写入配置中心。
  8. 安全底线client_secret / webhook_id 绝不暴露给前端,amount 服务端校验。