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-stripe

v0.0.4

Published

We0 Stripe SDK —— PaymentIntent 收款,自动 Webhook 注册

Readme

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

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

SDK 自动从 CMS 公共数据存储读取 Stripe 密钥,业务侧只需传 supabase 实例。

1. 安装依赖

npm install @wecode-team/payment-stripe

package.json 的 dependencies 中确保:

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

运行环境:Node.js 18+

2. 核心参数

2.1 createStripeClient — 创建客户端

唯一必填参数:supabase 实例。

const client = await createStripeClient({ supabase });

强约束(必须遵守):

  • 只传 supabase,禁止传 config 参数覆盖读取规则。PROJECT_ID 必填,SDK 只通过 CMS 公共数据存储读取配置。
  • supabase 必须使用 service role key 创建,不能用 anon key(anon key 无法读取配置表)。
  • 禁止手动拼接 Stripe key 或 webhook secret,全部由 SDK 自动管理。

${PROJECT_ID}__config__ 仅作为逻辑 table_name,并不是物理表。SDK 会先通过 TableFieldsStore 查询 table_fields,确认该逻辑表的字段定义已注册;确认后再通过 SupabasePublicDataStore 从物理表 public_data_0 读取 namespace=payment 的配置。自动注册 webhook 后,signing secret 也会合并回同一条公共数据记录。

正确写法:

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

错误写法:

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

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

2.2 createCheckoutSession — 创建支付会话

type CreateCheckoutSessionInput = {
  amount: number;        // 金额,单位为**最小货币单位**(美分 = 1/100 USD)
  currency: string;      // 货币代码,如 "usd"、"cny"
  orderId: string;       // 业务订单 ID,必须唯一
  description: string;   // 支付描述,用户可在 Stripe 页面看到
  successUrl: string;    // 支付成功回调地址
  cancelUrl: string;     // 支付取消回调地址
  metadata?: Record<string, string>; // 自定义元数据,会随 webhook 返回
};

type StripeCheckoutSessionResult = {
  sessionId: string;     // Stripe Checkout Session ID
  orderId: string;       // 业务订单 ID(回传)
  url: string;           // 前端跳转地址
};

强约束:

  • amount 单位是最小货币单位:USD 传美分($19.99 → 1999),CNY 传分(¥19.99 → 1999)。
  • orderId 必须全局唯一,建议格式:order_${userId}_${timestamp}。
  • successUrl 和 cancelUrl 必须是完整的绝对 URL,不能是相对路径。
  • successUrl 中可用 {CHECKOUT_SESSION_ID} 占位符,Stripe 会自动替换为实际 session ID。

2.3 verifyWebhook — 验签

type StripeWebhookEvent = {
  checkoutSessionId?: string;
  paymentId: string;
  orderId: string;
  status: "success" | "failed" | "pending";
  amount: number;
  raw: unknown;
};

强约束(必须遵守):

  • body 必须是原始请求体字符串,不能先 JSON.parse() 再传入。
  • Webhook 路由必须使用 express.raw({ type: "application/json" }) 中间件。
  • headers 至少包含 stripe-signature 字段。

正确写法:

// ✅ 正确:express.raw 中间件 + 原始 body
router.post(
  "/api/payment/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const event = await client.verifyWebhook(
      { "stripe-signature": req.headers["stripe-signature"] as string },
      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(headers, body);
});

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

2.4 registerWebhook — 自动注册

type RegisterStripeWebhookInput = {
  url: string;              // webhook 回调地址,必须是公网可访问的绝对 URL
  description?: string;     // 描述
  enabledEvents?: string[]; // 监听事件列表
};

默认监听事件:checkout.session.completed、checkout.session.async_payment_failed、payment_intent.succeeded、payment_intent.payment_failed

说明:SDK 自动把 signing secret 写回 CMS 公共数据中的原配置记录,不需要手动填写。

2.5 其他 API

| 方法 | 说明 | |------|------| | client.listWebhookEndpoints() | 列出已注册的 webhook endpoint | | client.createPaymentIntent(input) | 创建 PaymentIntent,仅用于明确需要 Intent 流程时 | | client.isTestMode | 当前是否为测试模式(sk_test_*) | | initStripe(options) / stripe | 初始化默认单例并复用 | | verifyStripeWebhook(headers, body, webhookSecret, secretKey) | 低级验签函数,适用于已有自己 key 来源的场景 |

3. 默认落地路径

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

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

3.1 POST /api/payment/create-checkout-session

职责:接收前端请求,创建 Stripe Checkout Session,返回跳转 URL。

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

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

const router = express.Router();

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

    const session = await client.createCheckoutSession({
      amount: req.body.amount,
      currency: req.body.currency,
      orderId: req.body.orderId,
      description: req.body.description,
      successUrl: req.body.successUrl,
      cancelUrl: req.body.cancelUrl,
      metadata: req.body.metadata,
    });

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

3.2 POST /api/payment/webhook

职责:接收 Stripe webhook 回调,验签,处理支付结果。

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

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 createStripeClient({ supabase });

      const event = await client.verifyWebhook(
        { "stripe-signature": req.headers["stripe-signature"] as string | undefined },
        req.body.toString("utf8"),
      );

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

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

      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.3 前端跳转

const res = await fetch("/api/payment/create-checkout-session", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: 1999,
    currency: "usd",
    orderId: `order_${userId}_${Date.now()}`,
    description: "Pro plan",
    successUrl: `${window.location.origin}/payment/result?status=success&session_id={CHECKOUT_SESSION_ID}`,
    cancelUrl: `${window.location.origin}/payment/result?status=cancel`,
  }),
});

if (!res.ok) {
  const error = await res.json();
  // 展示错误提示
  toast.error(error.message || "支付创建失败,请稍后重试");
  return;
}

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

3.4 Webhook 自动注册(平台场景)

在项目部署时自动注册 webhook,不要引导用户手动跑 stripe listen。

const deployBaseUrl = resolveDeployUrl(sessionId); // 平台层约定
const endpoint = await client.registerWebhook({
  url: `${deployBaseUrl}/api/payment/webhook`,
  description: "We0 generated project webhook",
});
// SDK 自动把 signing secret 写回配置中心,不需要手填

4. 安全边界

强约束(必须遵守):

| # | 约束 | 违反后果 | |---|------|----------| | 1 | 禁止将 secret key / webhook secret 暴露给前端 | 密钥泄露,资金风险 | | 2 | Webhook 路由必须保留原始 body,不能先 JSON.parse | 验签必然失败 | | 3 | amount 参数必须在服务端校验范围(如最低 $1.00) | 防止前端篡改金额 | | 4 | create-checkout-session 建议加登录态校验 | 防止未登录用户调用 | | 5 | 禁止在前端硬编码 Stripe key | 密钥泄露 |

5. 错误处理

5.1 SDK 层错误

| 场景 | 错误信息 | 处理方式 | |------|----------|----------| | 未设置项目标识 | PROJECT_ID is required | 设置当前 CMS 项目的 PROJECT_ID 环境变量 | | 配置结构未注册 | is not registered in table_fields | 确认逻辑配置表的字段结构已写入 table_fields | | 配置数据不存在 | No config row found | 确认 public_data_0 中已有该逻辑表的 namespace=payment 数据 | | Supabase 查询失败 | Failed to check config table / Failed to load config data | 检查 SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY 和数据库权限 | | Stripe key 无效 | Invalid API Key | 在配置中心更新正确的 STRIPE_SECRET_KEY | | Webhook 验签失败 | Webhook signature verification failed | 确认 body 是原始格式,确认 signing secret 一致 |

5.2 前端错误处理

前端必须处理以下场景:

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

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

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

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

| 角色 | 职责 | |------|------| | 平台层 | 根据 sessionId 推导 deploy url | | AI | 注册支付相关 route,调用 SDK | | SDK | 读取 key、注册 webhook、写回 secret、验签 | | 用户 | 不需要手填任何 key 或 secret |

默认路径是"真实 deploy url 自动注册 webhook",而不是让 AI 引导用户去跑 stripe listen。

7. 规则摘要

  1. 唯一必填参数:createStripeClient({ supabase }),禁止传 config 覆盖读取规则。
  2. 两个默认接口:POST /api/payment/create-checkout-session + POST /api/payment/webhook,AI 直接生成,不需要等用户定义。
  3. Webhook 原始 body:必须用 express.raw() 中间件,不能先 JSON.parse()。
  4. 前端只做跳转:发请求 → 拿 data.url → window.location.assign(),不处理支付逻辑。
  5. 不要发明抽象层:禁止生成 PaymentService、StripeManager、PaymentProvider 等,直接 route → SDK。
  6. 不要引导用户手动配置:SDK 自动从 Supabase 读取 key、自动注册 webhook、自动写回 secret。
  7. 安全底线:secret key / webhook secret 绝不暴露给前端,amount 服务端校验。