Install
$ agentstack add skill-paid-tw-skills-payuni-webhook ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
統一金流 Webhook 處理任務
你的任務是在用戶的專案中實作統一金流 Webhook 接收與處理功能。
串接 Checklist
- [ ] 框架確認 - 確認使用的框架
- [ ] 端點建立 - 建立 Webhook 接收端點
- [ ] 簽名驗證 - 實作 CheckCode 驗證
- [ ] 防重放 - 實作重複請求檢測
- [ ] 狀態更新 - 更新訂單狀態
- [ ] 測試驗證 - 驗證 Webhook 處理流程
Step 1: 確認專案環境
用戶輸入: $ARGUMENTS
詢問用戶:
- 框架類型:
- Next.js (App Router / Pages Router)
- Express / Fastify
- NestJS
- 其他
- 資料庫:用什麼來儲存訂單?
- PostgreSQL / MySQL
- MongoDB
- Prisma / Drizzle
- Supabase
- 其他
Step 2: 建立 Webhook 端點
Next.js App Router
// app/api/webhooks/payuni/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const config = {
hashKey: process.env.PAYUNI_HASH_KEY!,
hashIV: process.env.PAYUNI_HASH_IV!,
};
// 簽名驗證(使用 constant-time 比較防止 timing attack)
function verifyCheckCode(params: Record): boolean {
const { CheckCode, ...otherParams } = params;
if (!CheckCode) return false;
const sortedKeys = Object.keys(otherParams).sort();
const paramStr = sortedKeys.map(k => `${k}=${otherParams[k]}`).join('&');
const signStr = `HashKey=${config.hashKey}&${paramStr}&HashIV=${config.hashIV}`;
const calculated = crypto
.createHash('sha256')
.update(signStr)
.digest('hex')
.toUpperCase();
try {
return crypto.timingSafeEqual(
Buffer.from(calculated),
Buffer.from(CheckCode)
);
} catch {
return false;
}
}
export async function POST(request: NextRequest) {
try {
// 解析請求
const contentType = request.headers.get('content-type');
let params: Record;
if (contentType?.includes('application/json')) {
params = await request.json();
} else {
const formData = await request.formData();
params = Object.fromEntries(formData.entries()) as Record;
}
// 驗證簽名
if (!verifyCheckCode(params)) {
console.error('[Webhook] Invalid signature');
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// 取得訂單資訊
const { Status, MerchantOrderNo, TradeNo, TradeAmt } = params;
// TODO: 檢查是否已處理過此 TradeNo(防重放攻擊)
// const exists = await db.webhookLog.findUnique({ where: { tradeNo: TradeNo } });
// if (exists) return NextResponse.json({ success: true, message: 'Already processed' });
if (Status === 'SUCCESS') {
// TODO: 更新訂單狀態為已付款
// await db.order.update({
// where: { id: MerchantOrderNo },
// data: { status: 'paid', paymentDetails: { tradeNo: TradeNo, amount: TradeAmt } }
// });
console.log('[Webhook] Payment success:', MerchantOrderNo);
} else {
// TODO: 更新訂單狀態為失敗
console.log('[Webhook] Payment failed:', MerchantOrderNo);
}
// TODO: 記錄 webhook 請求(防重放)
// await db.webhookLog.create({ data: { tradeNo: TradeNo, processedAt: new Date() } });
return NextResponse.json({ success: true });
} catch (error) {
console.error('[Webhook] Error:', error);
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
// 健康檢查端點
export async function GET() {
return NextResponse.json({
message: 'PAYUNi Webhook endpoint',
timestamp: new Date().toISOString(),
});
}
Express
import express from 'express';
import crypto from 'crypto';
const router = express.Router();
router.post('/webhooks/payuni', async (req, res) => {
try {
const params = req.body;
// 驗證簽名
if (!verifyCheckCode(params)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { Status, MerchantOrderNo, TradeNo } = params;
if (Status === 'SUCCESS') {
// 更新訂單狀態
console.log('Payment success:', MerchantOrderNo);
}
res.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal error' });
}
});
export default router;
Step 3: 實作防重放攻擊
建立 webhook 請求記錄表:
CREATE TABLE webhook_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider VARCHAR(50) NOT NULL DEFAULT 'payuni',
trade_no VARCHAR(100) UNIQUE,
merchant_order_no VARCHAR(100),
checksum VARCHAR(64),
status VARCHAR(20) DEFAULT 'processing',
processed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_webhook_trade_no ON webhook_requests(trade_no);
CREATE INDEX idx_webhook_checksum ON webhook_requests(checksum);
防重放檢查邏輯
async function checkDuplicate(tradeNo: string): Promise {
const existing = await db.webhookRequest.findUnique({
where: { tradeNo }
});
return !!existing;
}
async function markProcessed(tradeNo: string): Promise {
await db.webhookRequest.create({
data: {
tradeNo,
provider: 'payuni',
processedAt: new Date(),
}
});
}
Step 4: 測試 Webhook
本地測試方法
- 使用 ngrok 暴露本地服務
``bash ngrok http 3000 ``
- 設定 NotifyURL
將 ngrok 提供的 HTTPS URL 設定為 NotifyURL
- 發起測試付款
在統一金流測試環境發起付款
- 檢查日誌
確認 Webhook 正確接收並處理
回調參數說明
成功付款通知參數
| 參數 | 說明 | |------|------| | Status | 付款狀態 SUCCESS / FAIL | | MerchantOrderNo | 商店訂單編號 | | TradeNo | PAYUNi 交易編號 | | TradeAmt | 交易金額 | | PaymentType | 付款方式 | | PayTime | 付款時間 | | CheckCode | 驗證碼 |
CheckCode 計算規則
1. 將所有參數(除了 CheckCode)按字母順序排序
2. 組成 key=value&key=value 的字串
3. 在開頭加上 HashKey=xxx&,結尾加上 &HashIV=xxx
4. 進行 SHA256 雜湊後轉大寫
安全注意事項
- 使用 HTTPS - 正式環境必須使用 HTTPS
- 驗證簽名 - 每次都要驗證 CheckCode
- 防重放攻擊 - 記錄已處理的 TradeNo
- Constant-time 比較 - 使用
timingSafeEqual防止 timing attack - 記錄日誌 - 記錄所有 Webhook 請求供除錯
詳細參考文件
- [程式碼範例](references/code-examples.md)
- [回應參數說明](references/response-parameters.md)
- [疑難排解](references/troubleshooting.md)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: paid-tw
- Source: paid-tw/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.