189 lines
5.6 KiB
TypeScript
189 lines
5.6 KiB
TypeScript
/**
|
|
* Pinduoduo Extension Public API Routes
|
|
* 拼多多浏览器插件端到端加密同步接口
|
|
*/
|
|
|
|
import { Router } from 'express'
|
|
import {
|
|
decryptPddExtensionPayload,
|
|
getPddExtensionPublicKeyInfo,
|
|
type PddEncryptedEnvelope,
|
|
} from '../services/platforms/pdd/pdd-crypto-service.js'
|
|
import {
|
|
getPddStoreById,
|
|
updatePddStoreCookieFromExtension,
|
|
verifyPddStoreSyncToken,
|
|
} from '../services/platforms/pdd/pdd-source-config-service.js'
|
|
import { PddClient } from '../services/platforms/pdd/pdd-client.js'
|
|
import { createHttpError } from '../utils/http.js'
|
|
import { logInfo, logWarn } from '../utils/logger.js'
|
|
|
|
const router = Router()
|
|
|
|
// 浏览器扩展跨域支持(支持 chrome-extension:// 等各种来源)
|
|
router.use((req, res, next) => {
|
|
const origin = req.headers.origin
|
|
if (origin) {
|
|
res.setHeader('Access-Control-Allow-Origin', origin)
|
|
} else {
|
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
}
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Shop-Token')
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
if (req.method === 'OPTIONS') {
|
|
res.sendStatus(204)
|
|
return
|
|
}
|
|
next()
|
|
})
|
|
|
|
/**
|
|
* GET /api/v1/pdd/extension/public-key
|
|
* 获取服务端 RSA-OAEP 2048 公钥信息(供浏览器插件加密使用)
|
|
*/
|
|
router.get('/public-key', (_req, res) => {
|
|
const info = getPddExtensionPublicKeyInfo()
|
|
res.json({
|
|
code: 0,
|
|
message: 'ok',
|
|
data: info,
|
|
})
|
|
})
|
|
|
|
/**
|
|
* POST /api/v1/pdd/extension/sync-cookie
|
|
* 浏览器插件上传端到端加密的最新 Cookie
|
|
*/
|
|
router.post('/sync-cookie', async (req, res, next) => {
|
|
try {
|
|
const envelope = (req.body?.envelope || req.body) as PddEncryptedEnvelope
|
|
|
|
// 1. 端到端安全解密
|
|
const decrypted = decryptPddExtensionPayload(envelope)
|
|
|
|
const clientToken = String(
|
|
req.headers['x-shop-token'] ||
|
|
req.body?.token ||
|
|
req.query?.token ||
|
|
decrypted.syncToken ||
|
|
'',
|
|
).trim()
|
|
|
|
if (!clientToken) {
|
|
throw createHttpError('未提供店铺专属配对密钥 (X-Shop-Token header 或 token 字段)', {
|
|
statusCode: 401,
|
|
errorCode: 'missing_shop_token',
|
|
})
|
|
}
|
|
|
|
// 2. 校验店铺配对密钥
|
|
const isTokenValid = verifyPddStoreSyncToken(decrypted.shopId, clientToken)
|
|
if (!isTokenValid) {
|
|
logWarn('[pdd-extension]', `插件尝试为店铺 ${decrypted.shopId} 上传 Cookie,但配对密钥无效`, {
|
|
shopId: decrypted.shopId,
|
|
ip: req.ip,
|
|
})
|
|
throw createHttpError('店铺配对密钥无效或已重置,请重新在后台复制配置', {
|
|
statusCode: 403,
|
|
errorCode: 'invalid_shop_token',
|
|
})
|
|
}
|
|
|
|
const store = getPddStoreById(decrypted.shopId)
|
|
if (!store) {
|
|
throw createHttpError(`店铺不存在: ${decrypted.shopId}`, {
|
|
statusCode: 404,
|
|
errorCode: 'store_not_found',
|
|
})
|
|
}
|
|
|
|
// 3. 诊断与实测 Cookie 有效性(轻量探测)
|
|
const cookieKeys = decrypted.cookie
|
|
.split(';')
|
|
.map((part) => part.trim().split('=')[0]?.trim())
|
|
.filter((k): k is string => Boolean(k))
|
|
const hasPassId = cookieKeys.includes('PASS_ID')
|
|
const hasRckk = cookieKeys.includes('rckk')
|
|
|
|
logInfo(
|
|
'[pdd-extension]',
|
|
`插件上报店铺 [${store.shopName}(${store.shopId})] Cookie,开始验证`,
|
|
{
|
|
shopId: store.shopId,
|
|
cookieLen: decrypted.cookie.length,
|
|
cookieKeyCount: cookieKeys.length,
|
|
hasPassId,
|
|
hasRckk,
|
|
cookieKeys: cookieKeys.slice(0, 15),
|
|
},
|
|
)
|
|
|
|
const testResult = await PddClient.testCookie({
|
|
cookie: decrypted.cookie,
|
|
userAgent: decrypted.userAgent || store.userAgent,
|
|
})
|
|
|
|
if (!testResult.valid) {
|
|
logWarn('[pdd-extension]', `插件上报的 Cookie 实测未通过: ${testResult.message}`, {
|
|
shopId: store.shopId,
|
|
hasPassId,
|
|
hasRckk,
|
|
cookieLen: decrypted.cookie.length,
|
|
})
|
|
|
|
const isSessionExpired =
|
|
testResult.message.includes('43001') ||
|
|
testResult.message.includes('会话已过期') ||
|
|
!hasPassId
|
|
|
|
const userGuidance = !hasPassId
|
|
? 'Cookie 中未包含拼多多核心会话标识 (PASS_ID)。请在当前浏览器打开并登录 mms.pinduoduo.com 商家后台,再点击插件上传。'
|
|
: isSessionExpired
|
|
? '拼多多商家会话已过期或失效 (43001)。请在浏览器刷新 mms.pinduoduo.com 页面重新登录,然后点击插件重新同步。'
|
|
: `Cookie 校验失败: ${testResult.message}`
|
|
|
|
res.status(400).json({
|
|
code: 400,
|
|
message: userGuidance,
|
|
data: {
|
|
valid: false,
|
|
shopId: store.shopId,
|
|
shopName: store.shopName,
|
|
hasPassId,
|
|
detail: testResult.message,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// 4. 更新店铺配置并自动置为有效
|
|
const updatedStore = await updatePddStoreCookieFromExtension(store.shopId, {
|
|
cookie: decrypted.cookie,
|
|
userAgent: decrypted.userAgent,
|
|
etag: decrypted.etag,
|
|
})
|
|
|
|
logInfo('[pdd-extension]', `插件成功自动同步店铺 [${store.shopName}(${store.shopId})] Cookie`, {
|
|
shopId: store.shopId,
|
|
totalOrders: testResult.totalOrders,
|
|
lastSyncAt: updatedStore.lastExtensionSyncAt,
|
|
})
|
|
|
|
res.json({
|
|
code: 0,
|
|
message: 'Cookie 同步成功并已生效',
|
|
data: {
|
|
shopId: updatedStore.shopId,
|
|
shopName: updatedStore.shopName,
|
|
valid: true,
|
|
totalOrders: testResult.totalOrders,
|
|
lastExtensionSyncAt: updatedStore.lastExtensionSyncAt,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
export default router
|