diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f364b34 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-console": ["warn", { "allow": ["warn", "error"] }], + "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "prefer-const": "error", + "no-var": "error", + "eqeqeq": ["error", "always"], + "curly": ["error", "all"], + "brace-style": ["error", "1tbs"], + "indent": ["error", 2], + "quotes": ["error", "single", { "avoidEscape": true }], + "semi": ["error", "always"], + "no-eval": "error", + "no-implied-eval": "error", + "no-new-func": "error", + "no-script-url": "error", + "no-unsafe-innerHTML": "off" + }, + "globals": { + "Deno": "readonly" + } +} diff --git a/env.example b/env.example index 3ec352a..3fc9623 100644 --- a/env.example +++ b/env.example @@ -17,10 +17,11 @@ LOG_LEVEL=info CORS_ORIGIN=* ALLOWED_ORIGIN=https://esim.cosr.eu.org -# 安全配置 -COOKIE_SECRET=your-secret-key-here # 受保护函数访问密钥(要求调用方在 Header x-esim-key 或 body.authKey / ?authKey 携带匹配值) -ACCESS_KEY=please_change_me # 必填:Server 与 Functions/BFF 共享的访问密钥 +# ⚠️ 必填:Server 与 Functions/BFF 共享的访问密钥 +# 🔐 生成强随机密钥: openssl rand -hex 32 +# ❌ 禁止使用默认值或简单密码 +ACCESS_KEY= # 可选:自定义API超时时间(毫秒) API_TIMEOUT=30000 diff --git a/netlify/edge-functions/bff-proxy.js b/netlify/edge-functions/bff-proxy.js index 2540b07..e31705f 100644 --- a/netlify/edge-functions/bff-proxy.js +++ b/netlify/edge-functions/bff-proxy.js @@ -1,7 +1,7 @@ /** * Netlify Edge Function: BFF Proxy * - 接收前端对 /bff/* 的请求 - * - 在服务端附加 x-esim-key(来自环境变量 ACCESS_KEY 或 ESIM_ACCESS_KEY) + * - 在服务端附加 x-esim-key(来自环境变量 ACCESS_KEY) * - 转发到对应的 /.netlify/functions/* 目标 */ @@ -23,7 +23,7 @@ export default async (request, context) => { // 从 Edge 运行时环境读取密钥 // Netlify Edge 使用 Deno 运行时 - const accessKey = (typeof Deno !== 'undefined' && Deno.env && (Deno.env.get('ACCESS_KEY') || Deno.env.get('ESIM_ACCESS_KEY'))) || ''; + const accessKey = (typeof Deno !== 'undefined' && Deno.env && Deno.env.get('ACCESS_KEY')) || ''; if (!accessKey) { return new Response(JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }), { status: 500, diff --git a/netlify/functions/_shared/middleware.js b/netlify/functions/_shared/middleware.js new file mode 100644 index 0000000..d1c5855 --- /dev/null +++ b/netlify/functions/_shared/middleware.js @@ -0,0 +1,308 @@ +/** + * Netlify Functions 统一中间件 + * 提供鉴权、CORS、错误处理等功能 + */ + +const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; +const ACCESS_KEY = process.env.ACCESS_KEY; + +// 启动时检查密钥 +if (!ACCESS_KEY) { + console.error('❌ 严重错误: ACCESS_KEY 未配置'); + console.error('💡 请在 Netlify 环境变量或 .env 文件中设置'); +} + +if (ACCESS_KEY === 'please_change_me') { + console.error('❌ 安全警告: ACCESS_KEY 使用了默认值,请立即修改'); +} + +/** + * 自定义错误类 + */ +class AuthError extends Error { + constructor(message, statusCode = 401) { + super(message); + this.name = 'AuthError'; + this.statusCode = statusCode; + } +} + +/** + * 提取请求中提供的认证密钥 + * @param {Object} event - Netlify event 对象 + * @returns {string} 认证密钥 + */ +function getProvidedKey(event) { + const lower = Object.fromEntries( + Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v]) + ); + + // 优先级: Header > Body > Query + const fromHeader = lower['x-esim-key'] || lower['x-app-key']; + if (fromHeader) return fromHeader; + + try { + const body = JSON.parse(event.body || '{}'); + if (body && typeof body.authKey === 'string') { + return body.authKey; + } + } catch {} + + const query = event.queryStringParameters || {}; + if (query.authKey) return query.authKey; + + return ''; +} + +/** + * 验证请求鉴权 + * @param {Object} event - Netlify event 对象 + * @returns {Object} 鉴权结果 { authorized: boolean, origin: string, preflight: boolean } + * @throws {AuthError} 鉴权失败时抛出 + */ +function authenticate(event) { + const lower = Object.fromEntries( + Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v]) + ); + const requestOrigin = lower.origin; + + // CORS 预检请求 + if (event.httpMethod === 'OPTIONS') { + // 验证来源 + if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { + throw new AuthError('Origin not allowed', 403); + } + return { preflight: true, origin: requestOrigin }; + } + + // 非预检请求:验证来源 + if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { + throw new AuthError('Origin not allowed', 403); + } + + // 验证 ACCESS_KEY 是否已配置 + if (!ACCESS_KEY) { + throw new AuthError('Server Misconfigured: ACCESS_KEY not configured', 500); + } + + // 提取并验证密钥 + const providedKey = getProvidedKey(event); + if (!providedKey || providedKey !== ACCESS_KEY) { + throw new AuthError('Unauthorized: Missing or invalid auth key', 401); + } + + return { authorized: true, origin: requestOrigin }; +} + +/** + * 生成标准响应头 + * @param {string} origin - 请求来源 + * @param {Object} additionalHeaders - 额外的响应头 + * @returns {Object} 响应头对象 + */ +function createHeaders(origin = ALLOWED_ORIGIN, additionalHeaders = {}) { + return { + 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-MFA-Signature, X-Esim-Key, X-App-Key', + 'Access-Control-Allow-Methods': 'POST, OPTIONS, GET', + 'Vary': 'Origin', + 'Content-Type': 'application/json', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + ...additionalHeaders + }; +} + +/** + * 统一错误处理 + * @param {Error} error - 错误对象 + * @param {string} context - 错误上下文 + * @returns {Object} Netlify response 对象 + */ +function handleError(error, context = 'unknown') { + const isDev = process.env.NODE_ENV === 'development'; + const statusCode = error.statusCode || (error.response?.status) || 500; + + // 结构化日志(生产环境应集成专业日志服务) + const logData = { + context, + message: error.message, + status: statusCode, + timestamp: new Date().toISOString() + }; + + if (isDev) { + logData.stack = error.stack; + logData.data = error.response?.data; + } + + console.error(`[${context}] Error:`, JSON.stringify(logData)); + + return { + statusCode, + headers: createHeaders(), + body: JSON.stringify({ + error: error.name || 'ServerError', + message: error.message, + ...(isDev && { stack: error.stack, context }) + }) + }; +} + +/** + * 输入验证中间件 + * @param {Object} schema - 验证规则对象 + * @param {Object} data - 待验证数据 + * @throws {AuthError} 验证失败时抛出 + */ +function validateInput(schema, data) { + const errors = []; + + Object.entries(schema).forEach(([key, rules]) => { + const value = data[key]; + + // required 检查 + if (rules.required && (value === undefined || value === null || value === '')) { + errors.push(`${key} is required`); + return; + } + + // 如果值为空且非必填,跳过后续验证 + if (!rules.required && (value === undefined || value === null)) { + return; + } + + // type 检查 + if (rules.type && typeof value !== rules.type) { + errors.push(`${key} must be of type ${rules.type}`); + } + + // minLength 检查 + if (rules.minLength && value.length < rules.minLength) { + errors.push(`${key} must be at least ${rules.minLength} characters`); + } + + // maxLength 检查 + if (rules.maxLength && value.length > rules.maxLength) { + errors.push(`${key} must not exceed ${rules.maxLength} characters`); + } + + // pattern 检查 + if (rules.pattern && !rules.pattern.test(value)) { + errors.push(`${key} has invalid format`); + } + + // enum 检查 + if (rules.enum && !rules.enum.includes(value)) { + errors.push(`${key} must be one of: ${rules.enum.join(', ')}`); + } + }); + + if (errors.length > 0) { + const error = new AuthError(`Validation failed: ${errors.join('; ')}`, 400); + error.validationErrors = errors; + throw error; + } +} + +/** + * 包装 Function Handler,自动处理鉴权和错误 + * @param {Function} handler - 业务逻辑处理函数 + * @param {Object} options - 配置选项 + * @returns {Function} 包装后的 handler + */ +function withAuth(handler, options = {}) { + return async (event, context) => { + const functionName = context.functionName || 'unknown'; + + try { + // 鉴权 + const auth = authenticate(event); + + // 预检请求直接返回 + if (auth.preflight) { + return { + statusCode: 200, + headers: createHeaders(auth.origin), + body: '' + }; + } + + // 解析请求体 + let parsedBody = {}; + if (event.body) { + try { + parsedBody = JSON.parse(event.body); + // 如果是数组格式,取第一个元素(兼容某些客户端) + if (Array.isArray(parsedBody)) { + parsedBody = parsedBody[0] || {}; + } + } catch (e) { + throw new AuthError('Invalid JSON body', 400); + } + } + + // 输入验证(如果提供了 schema) + if (options.validateSchema) { + validateInput(options.validateSchema, parsedBody); + } + + // 执行业务逻辑 + const result = await handler(event, context, { auth, body: parsedBody }); + + // 确保返回正确的响应格式 + if (!result.statusCode) { + return { + statusCode: 200, + headers: createHeaders(auth.origin), + body: JSON.stringify(result) + }; + } + + // 合并默认头 + result.headers = createHeaders(auth.origin, result.headers || {}); + return result; + + } catch (error) { + return handleError(error, functionName); + } + }; +} + +/** + * 创建带超时的 fetch 包装器 + * @param {string} url - 请求 URL + * @param {Object} options - fetch 选项 + * @param {number} timeout - 超时时间(毫秒) + * @returns {Promise} + */ +async function fetchWithTimeout(url, options = {}, timeout = 30000) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal + }); + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${timeout}ms`); + } + throw error; + } +} + +module.exports = { + authenticate, + createHeaders, + handleError, + validateInput, + withAuth, + fetchWithTimeout, + AuthError +}; diff --git a/netlify/functions/auto-activate-esim.js b/netlify/functions/auto-activate-esim.js index d0f2b64..5bf32da 100644 --- a/netlify/functions/auto-activate-esim.js +++ b/netlify/functions/auto-activate-esim.js @@ -4,265 +4,159 @@ */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event, context) => { - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const requestOrigin = lower['origin']; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lower['x-esim-key'] || lower['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; - // 设置CORS头 - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - // 处理预检请求 - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } - - // 只允许POST请求 - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ - error: 'Method Not Allowed', - message: '只允许POST请求' - }) - }; - } - - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } - - try { - // 解析请求体 - const requestBody = JSON.parse(event.body || '{}'); - const { activationCode, cookie, accessToken } = requestBody; - - if (!activationCode) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'activationCode参数不能为空' - }) - }; - } - - console.log('Auto Activation Request:', { - activationCode: activationCode, - hasCookie: !!cookie, - timestamp: new Date().toISOString() - }); - - // 调用Giffgaff激活API - const result = await callGiffgaffActivationAPI(activationCode, cookie, accessToken); - - if (result.success) { - console.log('Auto Activation Success:', { - message: result.message, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 200, - headers, - body: JSON.stringify({ - success: true, - message: result.message, - data: result.data - }) - }; - } else { - console.log('Auto Activation Failed:', { - message: result.message, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 400, - headers, - body: JSON.stringify({ - success: false, - error: 'Activation Failed', - message: result.message - }) - }; - } - - } catch (error) { - console.error('Auto Activation Error:', { - message: error.message, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 500, - headers, - body: JSON.stringify({ - success: false, - error: 'Internal Server Error', - message: '服务器内部错误' - }) - }; - } +// 输入验证schema +const autoActivateSchema = { + activationCode: { + required: true, + type: 'string', + minLength: 6, + maxLength: 50 + } }; +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 输入验证 + validateInput(autoActivateSchema, body); + + const { activationCode, cookie, accessToken } = body; + + // 调用Giffgaff激活API + const result = await callGiffgaffActivationAPI(activationCode, cookie, accessToken); + + if (result.success) { + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + message: result.message, + data: result.data + }) + }; + } else { + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + error: 'Activation Failed', + message: result.message + }) + }; + } +}, { validateSchema: autoActivateSchema }); + /** * 调用Giffgaff激活API - 完整流程 */ async function callGiffgaffActivationAPI(activationCode, cookieString, bearerToken) { - try { - const timestamp = Date.now(); - const defaultHeaders = { - 'Accept-Language': 'en-US,en;q=0.5', - 'DNT': '1', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36', - 'Sec-Ch-Ua': '"Not;A=Brand";v="99", "Chromium";v="139", "Google Chrome";v="139"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"' - }; - const cookieHeader = cookieString ? { Cookie: cookieString } : {}; - const bearerHeader = bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}; - const getHeaders = (extra = {}) => ({ ...defaultHeaders, ...cookieHeader, ...bearerHeader, ...extra }); - - // 第一步:验证激活码(Ajax校验) - console.log('Step 1: Validating activation code...'); - const validateUrl = `https://www.giffgaff.com/activate/validate-sim-code?code=${activationCode}&next-action=products&_=${timestamp}`; - - const validateResponse = await axios.get(validateUrl, { - headers: getHeaders({ - Accept: 'application/json, text/javascript, */*; q=0.01', - Referer: 'https://www.giffgaff.com/activate', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'X-Requested-With': 'XMLHttpRequest' - }), - timeout: 30000, - maxRedirects: 5 - }); + try { + const timestamp = Date.now(); + const defaultHeaders = { + 'Accept-Language': 'en-US,en;q=0.5', + 'DNT': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36', + 'Sec-Ch-Ua': '"Not;A=Brand";v="99", "Chromium";v="139", "Google Chrome";v="139"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"' + }; + const cookieHeader = cookieString ? { Cookie: cookieString } : {}; + const bearerHeader = bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}; + const getHeaders = (extra = {}) => ({ ...defaultHeaders, ...cookieHeader, ...bearerHeader, ...extra }); - console.log('Step 1 Response:', { - status: validateResponse.status, - data: validateResponse.data - }); + // 第一步:验证激活码 + const validateUrl = `https://www.giffgaff.com/activate/validate-sim-code?code=${activationCode}&next-action=products&_=${timestamp}`; - if (validateResponse.status !== 200) { - return { - success: false, - message: `激活码验证失败,HTTP状态码: ${validateResponse.status}` - }; - } + const validateResponse = await axios.get(validateUrl, { + headers: getHeaders({ + Accept: 'application/json, text/javascript, */*; q=0.01', + Referer: 'https://www.giffgaff.com/activate', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'X-Requested-With': 'XMLHttpRequest' + }), + timeout: 30000, + maxRedirects: 5 + }); - // 第二步:访问 /activate/swap 预览页面(保持会话) - console.log('Step 2: Loading swap preview page...'); - const swapPreview = await axios.get('https://www.giffgaff.com/activate/swap', { - headers: getHeaders({ - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - Referer: 'https://www.giffgaff.com/activate', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'same-origin', - 'Upgrade-Insecure-Requests': '1' - }), - timeout: 30000, - maxRedirects: 5 - }); - - // 第三步:GET swap-confirm 页面 - console.log('Step 3: Loading swap-confirm page...'); - const swapConfirmGet = await axios.get('https://www.giffgaff.com/activate/swap-confirm', { - headers: getHeaders({ - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - Referer: 'https://www.giffgaff.com/activate/swap', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'same-origin', - 'Upgrade-Insecure-Requests': '1' - }), - timeout: 30000, - maxRedirects: 5 - }); - - // 第四步:POST 确认(空体) - console.log('Step 4: Confirming swap (POST)...'); - // 从 cookie 中提取 XSRF-TOKEN(若存在) - let xsrf = null; - if (cookieString) { - const m = cookieString.match(/XSRF-TOKEN=([^;]+)/); - if (m) xsrf = decodeURIComponent(m[1]); - } - - const confirmPost = await axios.post('https://www.giffgaff.com/activate/swap-confirm', null, { - headers: getHeaders({ - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - Origin: 'https://www.giffgaff.com', - Referer: 'https://www.giffgaff.com/activate/swap-confirm', - 'Content-Type': 'application/x-www-form-urlencoded', - ...(xsrf ? { 'X-XSRF-TOKEN': xsrf } : {}), - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'same-origin', - 'Upgrade-Insecure-Requests': '1' - }), - timeout: 30000, - maxRedirects: 5 - }); - - // 若执行到此,表示触发了确认流程。返回成功提示。 - return { - success: true, - message: '已提交SIM替换确认,请等待新eSIM生效(通常几分钟)', - data: { - previewStatus: swapPreview.status, - confirmGetStatus: swapConfirmGet.status, - confirmPostStatus: confirmPost.status - } - }; - - } catch (error) { - console.error('Giffgaff Activation API Error:', error.message); - - if (error.response) { - return { - success: false, - message: `激活失败: ${error.response.status} - ${error.response.data?.message || '未知错误'}` - }; - } else { - return { - success: false, - message: `激活请求失败: ${error.message}` - }; - } + if (validateResponse.status !== 200) { + return { + success: false, + message: `激活码验证失败,HTTP状态码: ${validateResponse.status}` + }; } -} + + // 第二步:访问 /activate/swap 预览页面 + await axios.get('https://www.giffgaff.com/activate/swap', { + headers: getHeaders({ + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + Referer: 'https://www.giffgaff.com/activate', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1' + }), + timeout: 30000, + maxRedirects: 5 + }); + + // 第三步:GET swap-confirm 页面 + await axios.get('https://www.giffgaff.com/activate/swap-confirm', { + headers: getHeaders({ + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + Referer: 'https://www.giffgaff.com/activate/swap', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1' + }), + timeout: 30000, + maxRedirects: 5 + }); + + // 第四步:POST 确认 + let xsrf = null; + if (cookieString) { + const m = cookieString.match(/XSRF-TOKEN=([^;]+)/); + if (m) xsrf = decodeURIComponent(m[1]); + } + + const confirmPost = await axios.post('https://www.giffgaff.com/activate/swap-confirm', null, { + headers: getHeaders({ + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + Origin: 'https://www.giffgaff.com', + Referer: 'https://www.giffgaff.com/activate/swap-confirm', + 'Content-Type': 'application/x-www-form-urlencoded', + ...(xsrf ? { 'X-XSRF-TOKEN': xsrf } : {}), + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1' + }), + timeout: 30000, + maxRedirects: 5 + }); + + return { + success: true, + message: '已提交SIM替换确认,请等待新eSIM生效(通常几分钟)', + data: { + confirmPostStatus: confirmPost.status + } + }; + + } catch (error) { + if (error.response) { + return { + success: false, + message: `激活失败: ${error.response.status} - ${error.response.data?.message || '未知错误'}` + }; + } else { + return { + success: false, + message: `激活请求失败: ${error.message}` + }; + } + } +} diff --git a/netlify/functions/giffgaff-graphql.js b/netlify/functions/giffgaff-graphql.js index 76161a3..2bc4e8b 100644 --- a/netlify/functions/giffgaff-graphql.js +++ b/netlify/functions/giffgaff-graphql.js @@ -1,277 +1,145 @@ /** * Netlify Function: Giffgaff GraphQL API - * 处理GraphQL请求,解决CORS问题 + * 处理GraphQL请求,解决CORS问题 */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event, context) => { - // CORS 允许域(默认仅允许生产域名) - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const requestOrigin = lower['origin']; +// 输入验证schema +const graphqlSchema = { + query: { + required: true, + type: 'string', + minLength: 10 + }, + accessToken: { + required: false, + type: 'string', + minLength: 50 + } +}; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lower['x-esim-key'] || lower['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 解析请求体 + const { mfaSignature, mfaRef, query, variables, operationName, cookie } = body; - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-MFA-Signature', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; + // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) + const lowerCaseHeaders = Object.fromEntries( + Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) + ); + const authHeader = lowerCaseHeaders['authorization'] || ''; + let accessToken = body.accessToken; + if (!accessToken && authHeader.startsWith('Bearer ')) { + accessToken = authHeader.slice(7); + } - // 处理预检请求 - if (event.httpMethod === 'OPTIONS') { - // 仅允许指定来源的预检 - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } + if (!accessToken) { + throw new AuthError('accessToken是必需的', 400); + } - // 非预检:限制来源(无 Origin 视为服务端调用,放行) - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } + if (!query) { + throw new AuthError('GraphQL query是必需的', 400); + } - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } + // 构建请求头(使用真实的 iOS App UA 和头部) + const requestHeaders = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + 'Accept': '*/*', + 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', + 'Origin': 'https://publicapi.giffgaff.com', + 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br' + }; - // 只允许POST请求 - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ - error: 'Method Not Allowed', - message: '只允许POST请求' - }) - }; - } + // 针对 reserveESim / swapSim / eSimDownloadToken 需要设备元数据头 + const opName = operationName || ''; + const isReserve = /reserveESim\s*\(/.test(String(query || '')) || /reserveESim/i.test(opName); + const isSwap = /swapSim\s*\(/i.test(String(query || '')) || /swapSim/i.test(opName); + const isToken = /eSimDownloadToken\s*\(/i.test(String(query || '')) || /eSimDownloadToken/i.test(opName); + const isMfaChallenge = /simSwapMfaChallenge/i.test(String(query || '')) || /simSwapMfaChallenge/i.test(opName); + const needsAppHeaders = isReserve || isSwap || isToken || isMfaChallenge; + + if (needsAppHeaders) { + requestHeaders['x-gg-app-device-manufacturer'] = process.env.GG_APP_DEVICE_MANUFACTURER || 'Apple'; + requestHeaders['x-gg-app-os'] = process.env.GG_APP_OS || 'iOS'; + requestHeaders['x-gg-app-version'] = process.env.GG_APP_VERSION || '17.46.11'; + requestHeaders['x-gg-app-build-number'] = process.env.GG_APP_BUILD_NUMBER || '1332'; + requestHeaders['x-gg-app-os-version'] = process.env.GG_APP_OS_VERSION || '18.2'; + requestHeaders['apollographql-client-name'] = process.env.APOLLO_CLIENT_NAME || 'iOS 18.2'; + requestHeaders['apollographql-client-version'] = process.env.APOLLO_CLIENT_VERSION || '17.46.11 1332'; + requestHeaders['x-gg-app-bundle-version'] = process.env.GG_APP_BUNDLE_VERSION || 'v0'; + requestHeaders['x-gg-app-device-model'] = process.env.GG_APP_DEVICE_MODEL || 'iPhone SE'; + requestHeaders['x-gg-app-device-id'] = process.env.GG_APP_DEVICE_ID || 'iPhone12,8'; + requestHeaders['baggage'] = process.env.GG_BAGGAGE || 'client-tracking-ctx-id=d1c9ee72-573b-490e-a219-6b41992a5bdb'; try { - // 解析请求体(支持对象和数组两种格式) - let parsedBody = JSON.parse(event.body || '{}'); - - // 如果是数组格式,取第一个元素 - if (Array.isArray(parsedBody)) { - if (parsedBody.length === 0) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'GraphQL请求数组不能为空' - }) - }; - } - parsedBody = parsedBody[0]; - } - - const requestBody = parsedBody; - const { mfaSignature, mfaRef, query, variables, operationName, cookie } = requestBody; + const { randomUUID } = require('crypto'); + requestHeaders['x-request-id'] = randomUUID(); + } catch (_) {} + } - // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) - const lowerCaseHeaders = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const authHeader = lowerCaseHeaders['authorization'] || ''; - let accessToken = requestBody.accessToken; - if (!accessToken && authHeader.startsWith('Bearer ')) { - accessToken = authHeader.slice(7); - } + // 如果有MFA签名,添加到请求头 + if (mfaSignature) { + requestHeaders['X-MFA-Signature'] = mfaSignature; + requestHeaders['x-mfa-signature'] = mfaSignature; + } - if (!accessToken) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'accessToken是必需的' - }) - }; - } + // 构建GraphQL请求体 + const graphqlBody = { + query, + variables: variables || {}, + operationName: operationName || null + }; - if (!query) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'GraphQL query是必需的' - }) - }; - } + // 供失败时刷新令牌使用的 verify-cookie 地址 + const hostHdr = lowerCaseHeaders['x-forwarded-host'] || lowerCaseHeaders['host'] || ''; + const protoHdr = lowerCaseHeaders['x-forwarded-proto'] || 'https'; + const verifyCookieUrl = hostHdr ? `${protoHdr}://${hostHdr}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); - console.log('GraphQL Request:', { - operationName: operationName || 'Unknown', - hasVariables: !!variables, - hasMfaSignature: !!mfaSignature, - tokenLength: accessToken.length, - timestamp: new Date().toISOString() + // 调用Giffgaff GraphQL API + let response; + try { + response = await axios.post( + 'https://publicapi.giffgaff.com/gateway/graphql', + graphqlBody, + { headers: requestHeaders, timeout: 30000 } + ); + } catch (err) { + const status = err.response?.status; + const data = err.response?.data || {}; + const isUnauthorized = status === 401 || data?.error === 'unauthorized' || /invalid_token/i.test(String(data?.error || '')); + + // 失败 401 时尝试用 cookie 刷新后重试一次 + if (isUnauthorized && cookie) { + try { + const r = await axios.post(verifyCookieUrl, { cookie }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 15000 }); - // 构建请求头(使用真实的 iOS App UA 和头部) - const requestHeaders = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - 'Accept': '*/*', - 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', - 'Origin': 'https://publicapi.giffgaff.com', - 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br' - }; - - // 针对 reserveESim / swapSim / eSimDownloadToken 需要设备元数据头(使用 HAR 中的真实值) - const opName = operationName || ''; - const isReserve = /reserveESim\s*\(/.test(String(query || '')) || /reserveESim/i.test(opName); - const isSwap = /swapSim\s*\(/i.test(String(query || '')) || /swapSim/i.test(opName); - const isToken = /eSimDownloadToken\s*\(/i.test(String(query || '')) || /eSimDownloadToken/i.test(opName); - const isMfaChallenge = /simSwapMfaChallenge/i.test(String(query || '')) || /simSwapMfaChallenge/i.test(opName); - const needsAppHeaders = isReserve || isSwap || isToken || isMfaChallenge; - - if (needsAppHeaders) { - // 使用 HAR 抓包中的真实 iOS App 头部 - requestHeaders['x-gg-app-device-manufacturer'] = process.env.GG_APP_DEVICE_MANUFACTURER || 'Apple'; - requestHeaders['x-gg-app-os'] = process.env.GG_APP_OS || 'iOS'; - requestHeaders['x-gg-app-version'] = process.env.GG_APP_VERSION || '17.46.11'; - requestHeaders['x-gg-app-build-number'] = process.env.GG_APP_BUILD_NUMBER || '1332'; - requestHeaders['x-gg-app-os-version'] = process.env.GG_APP_OS_VERSION || '18.2'; - requestHeaders['apollographql-client-name'] = process.env.APOLLO_CLIENT_NAME || 'iOS 18.2'; - requestHeaders['apollographql-client-version'] = process.env.APOLLO_CLIENT_VERSION || '17.46.11 1332'; - requestHeaders['x-gg-app-bundle-version'] = process.env.GG_APP_BUNDLE_VERSION || 'v0'; - requestHeaders['x-gg-app-device-model'] = process.env.GG_APP_DEVICE_MODEL || 'iPhone SE'; - requestHeaders['x-gg-app-device-id'] = process.env.GG_APP_DEVICE_ID || 'iPhone12,8'; - requestHeaders['baggage'] = process.env.GG_BAGGAGE || 'client-tracking-ctx-id=d1c9ee72-573b-490e-a219-6b41992a5bdb'; - - try { - const { randomUUID } = require('crypto'); - requestHeaders['x-request-id'] = randomUUID(); - } catch (_) {} + if (r.data?.success && r.data?.accessToken) { + accessToken = r.data.accessToken; + requestHeaders['Authorization'] = `Bearer ${accessToken}`; + response = await axios.post( + 'https://publicapi.giffgaff.com/gateway/graphql', + graphqlBody, + { headers: requestHeaders, timeout: 30000 } + ); + } else { + throw err; } - - // 如果有MFA签名,添加到请求头(swapSim 只需要签名,不需要 ref) - if (mfaSignature) { - requestHeaders['X-MFA-Signature'] = mfaSignature; - requestHeaders['x-mfa-signature'] = mfaSignature; - } - - // 构建GraphQL请求体 - const graphqlBody = { - query, - variables: variables || {}, - operationName: operationName || null - }; - - // 供失败时刷新令牌使用的 verify-cookie 地址 - const hostHdr = lowerCaseHeaders['x-forwarded-host'] || lowerCaseHeaders['host'] || ''; - const protoHdr = lowerCaseHeaders['x-forwarded-proto'] || 'https'; - const verifyCookieUrl = hostHdr ? `${protoHdr}://${hostHdr}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); - - // 小范围调试日志:输出将发送的关键头(不含敏感 Authorization) - try { - const debugHeaders = { - 'X-MFA-Signature': requestHeaders['X-MFA-Signature'] || requestHeaders['x-mfa-signature'] || requestHeaders['X-GG-MFA-SIGNATURE'] || requestHeaders['x-gg-mfa-signature'] || null, - 'X-GG-MFA-REF': requestHeaders['X-GG-MFA-REF'] || requestHeaders['x-gg-mfa-ref'] || requestHeaders['X-MFA-REF'] || requestHeaders['x-mfa-ref'] || null, - 'x-gg-app-os': requestHeaders['x-gg-app-os'] || null, - 'x-gg-app-build-number': requestHeaders['x-gg-app-build-number'] || null - }; - console.log('GraphQL Outgoing Headers (debug):', debugHeaders); - } catch (_) {} - - // 调用Giffgaff GraphQL API(失败 401 时尝试用 cookie 刷新后重试一次) - let response; - try { - response = await axios.post( - 'https://publicapi.giffgaff.com/gateway/graphql', - graphqlBody, - { headers: requestHeaders, timeout: 30000 } - ); - } catch (err) { - const status = err.response?.status; - const data = err.response?.data || {}; - const isUnauthorized = status === 401 || data?.error === 'unauthorized' || /invalid_token/i.test(String(data?.error || '')); - if (isUnauthorized && cookie) { - try { - const r = await axios.post(verifyCookieUrl, { cookie }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 }); - if (r.data?.success && r.data?.accessToken) { - accessToken = r.data.accessToken; - requestHeaders['Authorization'] = `Bearer ${accessToken}`; - response = await axios.post( - 'https://publicapi.giffgaff.com/gateway/graphql', - graphqlBody, - { headers: requestHeaders, timeout: 30000 } - ); - } else { - throw err; - } - } catch (reErr) { - return { - statusCode: 401, - headers, - body: JSON.stringify({ - error: 'GraphQL Request Failed', - message: 'Access token expired. Please re-login with cookie.', - details: data, - needReLogin: true - }) - }; - } - } else { - throw err; - } - } - - console.log('GraphQL Success:', { - status: response.status, - hasData: !!response.data.data, - hasErrors: !!response.data.errors, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 200, - headers, - body: JSON.stringify(response.data) - }; - - } catch (error) { - console.error('GraphQL Error:', { - message: error.message, - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - timestamp: new Date().toISOString() - }); - - const status = error.response?.status || 500; - const errorMessage = error.response?.data?.message || error.message || '未知错误'; - - return { - statusCode: status, - headers, - body: JSON.stringify({ - error: 'GraphQL Request Failed', - message: errorMessage, - details: error.response?.data || null - }) - }; + } catch (reErr) { + throw new AuthError('Access token expired. Please re-login with cookie.', 401); + } + } else { + throw err; } -}; + } + + return { + statusCode: 200, + body: JSON.stringify(response.data) + }; +}, { validateSchema: graphqlSchema }); diff --git a/netlify/functions/giffgaff-mfa-challenge.js b/netlify/functions/giffgaff-mfa-challenge.js index 6e4df51..28a6787 100644 --- a/netlify/functions/giffgaff-mfa-challenge.js +++ b/netlify/functions/giffgaff-mfa-challenge.js @@ -1,344 +1,235 @@ /** * Netlify Function: Giffgaff MFA Challenge - * 处理MFA邮件验证码发送请求,解决CORS和403问题 + * 处理MFA邮件验证码发送请求,解决CORS和403问题 */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event, context) => { - // CORS 允许域(默认仅允许生产域名) - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lowerCaseHeaders = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const requestOrigin = lowerCaseHeaders['origin']; - - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lowerCaseHeaders['x-esim-key'] || lowerCaseHeaders['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; - - // 设置CORS头 - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - // 处理预检请求 - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } - - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } - - // 只允许POST请求 - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ - error: 'Method Not Allowed', - message: '只允许POST请求' - }) - }; - } - - try { - // 解析请求体 - const requestBody = JSON.parse(event.body || '{}'); - // 通过 authKey 校验已经在入口进行,这里不需要额外动作 - const { source = "esim", preferredChannels = ["EMAIL"], cookie } = requestBody; - - // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) - const lowerCaseHeaders = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const authHeader = lowerCaseHeaders['authorization'] || ''; - let accessToken = requestBody.accessToken; - if (!accessToken && authHeader.startsWith('Bearer ')) { - accessToken = authHeader.slice(7); - } - - if (!accessToken && !cookie) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'accessToken 或 cookie 至少提供一个' - }) - }; - } - - // 站点URL用于内部调用 verify-cookie(避免硬编码域名) - const lowerCaseHeadersForUrl = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const hostHeader = lowerCaseHeadersForUrl['x-forwarded-host'] || lowerCaseHeadersForUrl['host'] || ''; - const protoHeader = lowerCaseHeadersForUrl['x-forwarded-proto'] || 'https'; - const verifyCookieUrl = hostHeader ? `${protoHeader}://${hostHeader}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); - - // 如果提供cookie但没有accessToken,先尝试使用cookie获取accessToken - let mergedCookie = cookie || ''; - if (cookie) { - // 预热:访问 dashboard 刷新并合并 Set-Cookie,提高 id.giffgaff.com 接口成功率 - try { - const session = axios.create({ maxRedirects: 5, timeout: 30000, validateStatus: () => true }); - const dashResp = await session.get('https://www.giffgaff.com/dashboard', { - headers: { - 'Cookie': mergedCookie, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'zh-CN,zh;q=0.9', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Referer': 'https://www.giffgaff.com/auth/login/challenge?redirect=%2Fdashboard', - 'Upgrade-Insecure-Requests': '1', - 'DNT': '1' - } - }); - const setCookies = ([]).concat(dashResp.headers['set-cookie'] || []); - if (setCookies.length) { - mergedCookie = mergeSetCookies(mergedCookie, setCookies); - } - } catch (e) { - console.warn('Dashboard warm-up failed:', e.message); - } - // 若还没有 access token,则尝试一次 verify-cookie - if (!accessToken) { - try { - const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie: mergedCookie }, { - headers: { 'Content-Type': 'application/json' }, - timeout: 30000 - }); - if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { - accessToken = cookieVerifyResponse.data.accessToken; - console.log('Successfully obtained access token from merged cookie'); - } - } catch (cookieError) { - console.error('Failed to verify merged cookie:', cookieError.message); - } - } - } - - console.log('MFA Challenge Request:', { - source, - preferredChannels, - tokenLength: accessToken ? accessToken.length : 0, - hasCookie: !!cookie, - timestamp: new Date().toISOString() - }); - - // 调用Giffgaff MFA API,失败且令牌过期时,尝试用cookie刷新一次 - // 获取 CSRF(可提升 /v4/mfa/challenge/me 的通过率) - let csrfToken = null; - if (mergedCookie) { - try { - const csrfResp = await axios.get('https://id.giffgaff.com/auth/csrf', { - headers: { - 'Accept': 'application/json', - 'Cookie': mergedCookie, - 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', - 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br' - }, - timeout: 15000 - }); - csrfToken = csrfResp.data?.token || null; - } catch (e) { - console.warn('Fetch CSRF failed:', e.message); - } - } - - // 从 mergedCookie 中提取 XSRF-TOKEN,作为 x-xsrf-token 头 - let xxsrf = null; - if (mergedCookie) { - const m = String(mergedCookie).match(/XSRF-TOKEN=([^;]+)/); - if (m) xxsrf = decodeURIComponent(m[1]); - } - - // Web(Cookie)通道:auth/v3/mfa/challenge(method: 'text' | 'email') - const sendChallengeV3Cookie = async () => { - const method = Array.isArray(preferredChannels) && preferredChannels[0] === 'TEXT' ? 'text' : 'email'; - return axios.post( - 'https://id.giffgaff.com/auth/v3/mfa/challenge', - { method }, - { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Accept-Language': 'zh-CN,zh;q=0.9', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Origin': 'https://id.giffgaff.com', - 'Referer': 'https://id.giffgaff.com/auth/login/challenge', - 'Device': 'web', - ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), - ...(xxsrf ? { 'x-xsrf-token': xxsrf } : {}), - ...(mergedCookie ? { 'Cookie': mergedCookie } : {}) - }, - timeout: 30000 - } - ); - }; - - // App(Token)通道:v4 使用真实的 iOS App UA - const sendChallengeV4Token = async (token) => axios.post( - 'https://id.giffgaff.com/v4/mfa/challenge/me', - { source, preferredChannels }, - { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', - 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), - ...(mergedCookie ? { 'Cookie': mergedCookie } : {}) - }, - timeout: 30000 - } - ); - - let response; - try { - // 优先走 Cookie 的 Web 通道(更契合 Cookie 登录) - if (mergedCookie) { - try { - response = await sendChallengeV3Cookie(); - } catch (e) { - // 回退到 v4 + token - if (accessToken) response = await sendChallengeV4Token(accessToken); - else throw e; - } - } else { - response = await sendChallengeV4Token(accessToken); - } - } catch (err) { - const status = err.response?.status; - const data = err.response?.data || {}; - const isExpired = status === 401 && (data.error === 'invalid_token' || /expired/i.test(String(data.error_description || ''))); - if (isExpired && mergedCookie) { - try { - const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie: mergedCookie }, { - headers: { 'Content-Type': 'application/json' }, - timeout: 30000 - }); - if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { - let refreshed = cookieVerifyResponse.data.accessToken; - // 非JWT样式(无点号或长度过短)时,强制使用cookie通道 - const looksLikeJwt = typeof refreshed === 'string' && refreshed.includes('.') && refreshed.length > 200; - if (!looksLikeJwt) { - refreshed = null; - } - console.log('Refreshed access token via cookie, retrying MFA challenge'); - if (refreshed) response = await sendChallengeV4Token(refreshed); - else response = await sendChallengeV3Cookie(); - } else { - throw err; - } - } catch (reErr) { - // 多次失败提示客户端需要刷新登录/重新获取Cookie - return { - statusCode: 401, - headers, - body: JSON.stringify({ - error: 'MFA Challenge Failed', - message: 'Access token expired. Please re-login with cookie.', - details: data, - needReLogin: true - }) - }; - } - } else { - throw err; - } - } - - console.log('MFA Challenge Success:', { - status: response.status, - hasRef: !!response.data.ref, - channel: Array.isArray(preferredChannels) ? preferredChannels[0] : 'EMAIL', - timestamp: new Date().toISOString() - }); - - return { - statusCode: 200, - headers, - body: JSON.stringify(response.data) - }; - - } catch (error) { - console.error('MFA Challenge Error:', { - message: error.message, - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - timestamp: new Date().toISOString() - }); - - const status = error.response?.status || 500; - const errorMessage = error.response?.data?.message || error.message || '未知错误'; - - return { - statusCode: status, - headers, - body: JSON.stringify({ - error: 'MFA Challenge Failed', - message: errorMessage, - details: error.response?.data || null - }) - }; - } +// 输入验证schema +const mfaChallengeSchema = { + source: { + required: false, + type: 'string', + enum: ['esim', 'web', 'app'] + }, + preferredChannels: { + required: false, + type: 'object' + } }; +exports.handler = withAuth(async (event, context, { auth, body }) => { + const { source = "esim", preferredChannels = ["EMAIL"], cookie } = body; + + // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) + const lowerCaseHeaders = Object.fromEntries( + Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) + ); + const authHeader = lowerCaseHeaders['authorization'] || ''; + let accessToken = body.accessToken; + if (!accessToken && authHeader.startsWith('Bearer ')) { + accessToken = authHeader.slice(7); + } + + if (!accessToken && !cookie) { + throw new AuthError('accessToken 或 cookie 至少提供一个', 400); + } + + // 站点URL用于内部调用 verify-cookie + const hostHeader = lowerCaseHeaders['x-forwarded-host'] || lowerCaseHeaders['host'] || ''; + const protoHeader = lowerCaseHeaders['x-forwarded-proto'] || 'https'; + const verifyCookieUrl = hostHeader ? `${protoHeader}://${hostHeader}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); + + // 如果提供cookie但没有accessToken,先尝试使用cookie获取accessToken + let mergedCookie = cookie || ''; + if (cookie) { + // 预热:访问 dashboard 刷新并合并 Set-Cookie + try { + const session = axios.create({ maxRedirects: 5, timeout: 30000, validateStatus: () => true }); + const dashResp = await session.get('https://www.giffgaff.com/dashboard', { + headers: { + 'Cookie': mergedCookie, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + 'Referer': 'https://www.giffgaff.com/auth/login/challenge?redirect=%2Fdashboard', + 'Upgrade-Insecure-Requests': '1', + 'DNT': '1' + } + }); + const setCookies = ([]).concat(dashResp.headers['set-cookie'] || []); + if (setCookies.length) { + mergedCookie = mergeSetCookies(mergedCookie, setCookies); + } + } catch (e) { + // 预热失败不影响主流程 + } + + // 若还没有 access token,则尝试一次 verify-cookie + if (!accessToken) { + try { + const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie: mergedCookie }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000 + }); + if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { + accessToken = cookieVerifyResponse.data.accessToken; + } + } catch (cookieError) { + // Cookie验证失败不影响主流程 + } + } + } + + // 获取 CSRF(可提升 /v4/mfa/challenge/me 的通过率) + let csrfToken = null; + if (mergedCookie) { + try { + const csrfResp = await axios.get('https://id.giffgaff.com/auth/csrf', { + headers: { + 'Accept': 'application/json', + 'Cookie': mergedCookie, + 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', + 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br' + }, + timeout: 15000 + }); + csrfToken = csrfResp.data?.token || null; + } catch (e) { + // CSRF获取失败不影响主流程 + } + } + + // 从 mergedCookie 中提取 XSRF-TOKEN + let xxsrf = null; + if (mergedCookie) { + const m = String(mergedCookie).match(/XSRF-TOKEN=([^;]+)/); + if (m) xxsrf = decodeURIComponent(m[1]); + } + + // Web(Cookie)通道:auth/v3/mfa/challenge + const sendChallengeV3Cookie = async () => { + const method = Array.isArray(preferredChannels) && preferredChannels[0] === 'TEXT' ? 'text' : 'email'; + return axios.post( + 'https://id.giffgaff.com/auth/v3/mfa/challenge', + { method }, + { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + 'Origin': 'https://id.giffgaff.com', + 'Referer': 'https://id.giffgaff.com/auth/login/challenge', + 'Device': 'web', + ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), + ...(xxsrf ? { 'x-xsrf-token': xxsrf } : {}), + ...(mergedCookie ? { 'Cookie': mergedCookie } : {}) + }, + timeout: 30000 + } + ); + }; + + // App(Token)通道:v4 + const sendChallengeV4Token = async (token) => axios.post( + 'https://id.giffgaff.com/v4/mfa/challenge/me', + { source, preferredChannels }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', + 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + ...(mergedCookie ? { 'Cookie': mergedCookie } : {}) + }, + timeout: 30000 + } + ); + + let response; + try { + // 优先走 Cookie 的 Web 通道 + if (mergedCookie) { + try { + response = await sendChallengeV3Cookie(); + } catch (e) { + // 回退到 v4 + token + if (accessToken) response = await sendChallengeV4Token(accessToken); + else throw e; + } + } else { + response = await sendChallengeV4Token(accessToken); + } + } catch (err) { + const status = err.response?.status; + const data = err.response?.data || {}; + const isExpired = status === 401 && (data.error === 'invalid_token' || /expired/i.test(String(data.error_description || ''))); + + if (isExpired && mergedCookie) { + try { + const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie: mergedCookie }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000 + }); + + if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { + let refreshed = cookieVerifyResponse.data.accessToken; + const looksLikeJwt = typeof refreshed === 'string' && refreshed.includes('.') && refreshed.length > 200; + + if (!looksLikeJwt) { + refreshed = null; + } + + if (refreshed) response = await sendChallengeV4Token(refreshed); + else response = await sendChallengeV3Cookie(); + } else { + throw err; + } + } catch (reErr) { + throw new AuthError('Access token expired. Please re-login with cookie.', 401); + } + } else { + throw err; + } + } + + return { + statusCode: 200, + body: JSON.stringify(response.data) + }; +}, { validateSchema: mfaChallengeSchema }); + // 合并原始 Cookie 与 Set-Cookie 数组 function mergeSetCookies(originalCookieHeader, setCookieArray) { - const jar = new Map(); - // 先装入原始 cookie - String(originalCookieHeader || '') - .split(';') - .map(s => s.trim()) - .filter(Boolean) - .forEach(kv => { - const eq = kv.indexOf('='); - if (eq > 0) { - const k = kv.slice(0, eq).trim(); - const v = kv.slice(eq + 1).trim(); - if (k) jar.set(k, v); - } - }); - // 处理 set-cookie 覆盖 - for (const sc of setCookieArray) { - const pair = String(sc).split(';')[0]; - const eq = pair.indexOf('='); - if (eq > 0) { - const k = pair.slice(0, eq).trim(); - const v = pair.slice(eq + 1).trim(); - if (k) jar.set(k, v); - } + const jar = new Map(); + // 先装入原始 cookie + String(originalCookieHeader || '') + .split(';') + .map(s => s.trim()) + .filter(Boolean) + .forEach(kv => { + const eq = kv.indexOf('='); + if (eq > 0) { + const k = kv.slice(0, eq).trim(); + const v = kv.slice(eq + 1).trim(); + if (k) jar.set(k, v); + } + }); + // 处理 set-cookie 覆盖 + for (const sc of setCookieArray) { + const pair = String(sc).split(';')[0]; + const eq = pair.indexOf('='); + if (eq > 0) { + const k = pair.slice(0, eq).trim(); + const v = pair.slice(eq + 1).trim(); + if (k) jar.set(k, v); } - return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; '); + } + return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; '); } diff --git a/netlify/functions/giffgaff-mfa-validation.js b/netlify/functions/giffgaff-mfa-validation.js index d7cfb6f..9b67d95 100644 --- a/netlify/functions/giffgaff-mfa-validation.js +++ b/netlify/functions/giffgaff-mfa-validation.js @@ -4,218 +4,116 @@ */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event, context) => { - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lowerCaseHeaders = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const requestOrigin = lowerCaseHeaders['origin']; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lowerCaseHeaders['x-esim-key'] || lowerCaseHeaders['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; - - // 设置CORS头 - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - // 处理预检请求 - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } - - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } - - // 只允许POST请求 - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ - error: 'Method Not Allowed', - message: '只允许POST请求' - }) - }; - } - - try { - // 解析请求体 - const requestBody = JSON.parse(event.body || '{}'); - const { ref, code, cookie } = requestBody; - - // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) - const lowerCaseHeaders = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const authHeader = lowerCaseHeaders['authorization'] || ''; - let accessToken = requestBody.accessToken; - if (!accessToken && authHeader.startsWith('Bearer ')) { - accessToken = authHeader.slice(7); - } - - if ((!accessToken && !cookie) || !ref || !code) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'accessToken 或 cookie 与 ref, code 都是必需的' - }) - }; - } - - // 站点URL用于内部调用 verify-cookie(避免硬编码域名) - const lowerCaseHeadersForUrl = Object.fromEntries( - Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) - ); - const hostHeader = lowerCaseHeadersForUrl['x-forwarded-host'] || lowerCaseHeadersForUrl['host'] || ''; - const protoHeader = lowerCaseHeadersForUrl['x-forwarded-proto'] || 'https'; - const verifyCookieUrl = hostHeader ? `${protoHeader}://${hostHeader}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); - - // 如果提供cookie但没有accessToken,先尝试使用cookie获取accessToken - if (cookie && !accessToken) { - try { - // 调用verify-cookie函数获取accessToken - const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie }, { - headers: { 'Content-Type': 'application/json' }, - timeout: 30000 - }); - - if (cookieVerifyResponse.data && cookieVerifyResponse.data.success && cookieVerifyResponse.data.accessToken) { - accessToken = cookieVerifyResponse.data.accessToken; - console.log('Successfully obtained access token from cookie'); - } - } catch (cookieError) { - console.error('Failed to verify cookie:', cookieError.message); - // 继续使用原始cookie - } - } - - console.log('MFA Validation Request:', { - ref, - codeLength: code.length, - tokenLength: accessToken ? accessToken.length : 0, - hasCookie: !!cookie, - timestamp: new Date().toISOString() - }); - - // 调用Giffgaff MFA验证API,失败且令牌过期时,尝试用cookie刷新一次 - const sendValidation = async (token) => axios.post( - 'https://id.giffgaff.com/v4/mfa/validation', - { ref, code }, - { - headers: (() => { - const h = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', - 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br' - }; - if (token) h['Authorization'] = `Bearer ${token}`; - if (!token && cookie) h['Cookie'] = cookie; - return h; - })(), - timeout: 30000 - } - ); - - let response; - try { - response = await sendValidation(accessToken); - } catch (err) { - const status = err.response?.status; - const data = err.response?.data || {}; - const isExpired = status === 401 && (data.error === 'invalid_token' || /expired/i.test(String(data.error_description || ''))); - if (isExpired && cookie) { - try { - const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie }, { - headers: { 'Content-Type': 'application/json' }, - timeout: 30000 - }); - if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { - const refreshed = cookieVerifyResponse.data.accessToken; - console.log('Refreshed access token via cookie, retrying MFA validation'); - response = await sendValidation(refreshed); - } else { - throw err; - } - } catch (reErr) { - return { - statusCode: 401, - headers, - body: JSON.stringify({ - error: 'MFA Validation Failed', - message: 'Access token expired. Please re-login with cookie.', - details: data, - needReLogin: true - }) - }; - } - } else { - throw err; - } - } - - console.log('MFA Validation Success:', { - status: response.status, - hasSignature: !!response.data.signature, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 200, - headers, - body: JSON.stringify(response.data) - }; - - } catch (error) { - console.error('MFA Validation Error:', { - message: error.message, - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - timestamp: new Date().toISOString() - }); - - const status = error.response?.status || 500; - const errorMessage = error.response?.data?.message || error.message || '未知错误'; - - return { - statusCode: status, - headers, - body: JSON.stringify({ - error: 'MFA Validation Failed', - message: errorMessage, - details: error.response?.data || null - }) - }; - } +// 输入验证schema +const mfaValidationSchema = { + ref: { + required: true, + type: 'string', + minLength: 10 + }, + code: { + required: true, + type: 'string', + minLength: 4, + maxLength: 10 + } }; + +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 输入验证 + validateInput(mfaValidationSchema, body); + + const { ref, code, cookie } = body; + + // 从请求体或 Authorization 头提取 accessToken(兼容两种方式) + const lowerCaseHeaders = Object.fromEntries( + Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v]) + ); + const authHeader = lowerCaseHeaders['authorization'] || ''; + let accessToken = body.accessToken; + if (!accessToken && authHeader.startsWith('Bearer ')) { + accessToken = authHeader.slice(7); + } + + if (!accessToken && !cookie) { + throw new AuthError('accessToken 或 cookie 至少提供一个', 400); + } + + // 站点URL用于内部调用 verify-cookie + const hostHeader = lowerCaseHeaders['x-forwarded-host'] || lowerCaseHeaders['host'] || ''; + const protoHeader = lowerCaseHeaders['x-forwarded-proto'] || 'https'; + const verifyCookieUrl = hostHeader ? `${protoHeader}://${hostHeader}/.netlify/functions/verify-cookie` : ((process.env.URL || '').replace(/\/$/, '') + '/.netlify/functions/verify-cookie'); + + // 如果提供cookie但没有accessToken,先尝试使用cookie获取accessToken + if (cookie && !accessToken) { + try { + const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000 + }); + + if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { + accessToken = cookieVerifyResponse.data.accessToken; + } + } catch (cookieError) { + // Cookie验证失败不影响主流程 + } + } + + // 调用Giffgaff MFA验证API + const sendValidation = async (token) => axios.post( + 'https://id.giffgaff.com/v4/mfa/validation', + { ref, code }, + { + headers: (() => { + const h = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': process.env.GG_USER_AGENT || 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0', + 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br' + }; + if (token) h['Authorization'] = `Bearer ${token}`; + if (!token && cookie) h['Cookie'] = cookie; + return h; + })(), + timeout: 30000 + } + ); + + let response; + try { + response = await sendValidation(accessToken); + } catch (err) { + const status = err.response?.status; + const data = err.response?.data || {}; + const isExpired = status === 401 && (data.error === 'invalid_token' || /expired/i.test(String(data.error_description || ''))); + + if (isExpired && cookie) { + try { + const cookieVerifyResponse = await axios.post(verifyCookieUrl, { cookie }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000 + }); + + if (cookieVerifyResponse.data?.success && cookieVerifyResponse.data?.accessToken) { + const refreshed = cookieVerifyResponse.data.accessToken; + response = await sendValidation(refreshed); + } else { + throw err; + } + } catch (reErr) { + throw new AuthError('Access token expired. Please re-login with cookie.', 401); + } + } else { + throw err; + } + } + + return { + statusCode: 200, + body: JSON.stringify(response.data) + }; +}, { validateSchema: mfaValidationSchema }); diff --git a/netlify/functions/giffgaff-sms-activate.js b/netlify/functions/giffgaff-sms-activate.js index ba633fe..53127c0 100644 --- a/netlify/functions/giffgaff-sms-activate.js +++ b/netlify/functions/giffgaff-sms-activate.js @@ -1,289 +1,251 @@ /** * Netlify Function: Giffgaff SMS Activate (end-to-end) - * 输入短信验证码后,后台自动完成:MFA校验 → 预订eSIM(如需)→ 网页激活 → 轮询获取LPA + * 输入短信验证码后,后台自动完成:MFA校验 → 预订eSIM(如需)→ 网页激活 → 轮询获取LPA */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event, context) => { - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const requestOrigin = lower['origin']; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lower['x-esim-key'] || lower['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } - if (event.httpMethod !== 'POST') { - return { statusCode: 405, headers, body: JSON.stringify({ error: 'Method Not Allowed', message: '只允许POST请求' }) }; - } - - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - - // 鉴权参数校验 - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } - - try { - const req = JSON.parse(event.body || '{}'); - const { - ref, // MFA ref(由 simSwapMfaChallenge 返回) - code, // 用户输入的短信验证码 - accessToken, // OAuth Bearer Token(推荐提供) - cookie, // giffgaff 登录 Cookie(用于网页激活流程与token刷新) - memberId, // 可选:若前端已获取 - ssn, // 可选:已有预订的 eSIM SSN - activationCode // 可选:已有预订的 eSIM Activation Code - } = req; - - if (!ref || !code) { - return { statusCode: 400, headers, body: JSON.stringify({ error: 'Bad Request', message: 'ref 与 code 必须提供' }) }; - } - - // 站点URL用于内部调用其他函数(避免硬编码域名) - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const host = lower['x-forwarded-host'] || lower['host'] || ''; - const proto = lower['x-forwarded-proto'] || 'https'; - const baseUrl = host ? `${proto}://${host}` : String(process.env.URL || '').replace(/\/$/, ''); - - // 统一创建 GraphQL 客户端 - const createGraphql = (token, extraHeaders = {}) => (body) => axios.post( - 'https://publicapi.giffgaff.com/gateway/graphql', - { ...body, mfaRef: ref }, - { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': token ? `Bearer ${token}` : undefined, - 'Origin': 'https://www.giffgaff.com', - 'Referer': 'https://www.giffgaff.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - // 模拟App头,提升通过率 - 'x-gg-app-os': process.env.GG_APP_OS || 'iOS', - 'x-gg-app-os-version': process.env.GG_APP_OS_VERSION || '18.2', - 'x-gg-app-build-number': process.env.GG_APP_BUILD_NUMBER || '1321', - 'x-gg-app-device-manufacturer': process.env.GG_APP_DEVICE_MANUFACTURER || 'Apple', - 'x-gg-app-device-model': process.env.GG_APP_DEVICE_MODEL || 'iPhone SE', - ...extraHeaders - }, - timeout: 30000 - } - ); - - // 1) 获取 CSRF Token(用于 /v4/mfa/validation) - let csrfToken = null; - if (cookie) { - try { - const csrfResp = await axios.get('https://id.giffgaff.com/auth/csrf', { - headers: { - 'Accept': 'application/json', - 'Cookie': cookie, - 'User-Agent': 'giffgaff/1321 CFNetwork/1568.300.101 Darwin/24.2.0' - }, - timeout: 15000 - }); - csrfToken = csrfResp.data?.token || null; - } catch (e) { - // 不中断流程;若缺失仍尝试验证 - console.warn('Fetch CSRF failed:', e.message); - } - } - - // 2) 校验短信验证码,获取签名 - // 优先尝试 token 通道;失败401且有 cookie 时,回退到 Web 通道 - const buildV4Headers = (token, ck) => ({ - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'giffgaff/1321 CFNetwork/1568.300.101 Darwin/24.2.0', - 'Origin': 'https://www.giffgaff.com', - 'Referer': 'https://www.giffgaff.com/', - ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), - ...(ck ? { 'Cookie': ck } : {}) - }); - - // 从 cookie 中提取 XSRF-TOKEN - let xxsrf = null; - if (cookie) { - const m = String(cookie).match(/XSRF-TOKEN=([^;]+)/); - if (m) xxsrf = decodeURIComponent(m[1]); - } - - const buildV3Headers = (ck) => ({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Accept-Language': 'zh-CN,zh;q=0.9', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Origin': 'https://id.giffgaff.com', - 'Referer': 'https://id.giffgaff.com/auth/login/challenge', - 'Device': 'web', - ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), - ...(xxsrf ? { 'x-xsrf-token': xxsrf } : {}), - ...(ck ? { 'Cookie': ck } : {}) - }); - - let validationResp; - try { - validationResp = await axios.post( - 'https://id.giffgaff.com/v4/mfa/validation', - { ref, code }, - { headers: buildV4Headers(accessToken, cookie), timeout: 30000 } - ); - } catch (err) { - const status = err.response?.status; - const detail = err.response?.data; - const tokenExpired = status === 401; - if (tokenExpired && cookie) { - try { - // 回退到 Web v3 验证,不带 Authorization - validationResp = await axios.post( - 'https://id.giffgaff.com/auth/v3/mfa/validation', - { ref, code }, - { headers: buildV3Headers(cookie), timeout: 30000 } - ); - } catch (err2) { - const s2 = err2.response?.status || 500; - return { statusCode: s2, headers, body: JSON.stringify({ error: 'MFA Validation Failed', details: err2.response?.data || detail }) }; - } - } else { - const s = status || 500; - return { statusCode: s, headers, body: JSON.stringify({ error: 'MFA Validation Failed', details: detail || null }) }; - } - } - - const mfaSignature = validationResp.data?.signature; - if (!mfaSignature) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'SignatureMissing', message: '未获取到MFA签名' }) }; - } - - // 3) 若无 ssn/activationCode,则:获取 memberId → 预订 eSIM(SWITCH) - let currentMemberId = memberId || null; - let currentSSN = ssn || null; - let currentActivationCode = activationCode || null; - - const gql = createGraphql(accessToken); - - if (!currentMemberId) { - const q = { - query: `query getMemberProfileAndSim { memberProfile { id } sim { status } }`, - variables: {}, - operationName: 'getMemberProfileAndSim' - }; - const r = await gql(q); - currentMemberId = r.data?.data?.memberProfile?.id || null; - if (!currentMemberId) { - return { statusCode: 400, headers, body: JSON.stringify({ error: 'MemberIdMissing', message: '无法获取会员ID' }) }; - } - } - - if (!currentSSN || !currentActivationCode) { - const reserveBody = { - query: `mutation reserveESim($input: ESimReservationInput!) { reserveESim: reserveESim(input: $input) { id memberId status esim { ssn activationCode deliveryStatus __typename } __typename } }`, - variables: { input: { memberId: currentMemberId, userIntent: 'SWITCH' } }, - operationName: 'reserveESim' - }; - - const r = await createGraphql(accessToken, { 'X-MFA-Signature': mfaSignature })(reserveBody); - const reservation = r.data?.data?.reserveESim; - if (!reservation?.esim) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'ReserveFailed', details: r.data }) }; - } - currentSSN = reservation.esim.ssn; - currentActivationCode = reservation.esim.activationCode; - } - - // 4) 执行 swapSim,将预订的 eSIM 正式替换为新卡(App 流程关键步骤) - let swapRef = null; - try { - // 先发起 GraphQL 的 simSwapMfaChallenge,获取用于 swap 的专属 ref - try { - const chBody = { query: `mutation simSwapMfaChallenge { simSwapMfaChallenge { ref methods { value channel __typename } __typename } }`, variables: {}, operationName: 'simSwapMfaChallenge' }; - const ch = await createGraphql(accessToken)({ ...chBody }); - swapRef = ch.data?.data?.simSwapMfaChallenge?.ref || null; - } catch (_) {} - - const swapBody = { - query: `mutation SwapSim($activationCode: String!, $mfaSignature: String!) { swapSim(activationCode: $activationCode, mfaSignature: $mfaSignature) { old { ssn activationCode __typename } new { ssn activationCode __typename } __typename } }`, - variables: { activationCode: currentActivationCode, mfaSignature }, - operationName: 'SwapSim' - }; - const rs = await createGraphql(accessToken)({ ...swapBody, mfaRef: swapRef || ref }); - const sw = rs.data?.data?.swapSim; - if (sw?.new?.ssn) { - currentSSN = sw.new.ssn; - currentActivationCode = sw.new.activationCode || currentActivationCode; - } - } catch (e) { - // 允许继续轮询(有些场景 swapSim 会由后端异步完成) - } - - // 5) 直接走 GraphQL 下载令牌(多数 App 流程不依赖网页 /activate) - // 这里不再调用网页 auto-activate-esim,避免 403 和与 App 流不一致 - // 轮询获取 LPA(最长 ~120秒) - const downloadQuery = { - query: `query eSimDownloadToken($ssn: String!) { eSimDownloadToken(ssn: $ssn) { id host matchingId lpaString __typename } }`, - variables: { ssn: currentSSN }, - operationName: 'eSimDownloadToken' - }; - - const deadline = Date.now() + 120000; - let lastData = null; - while (Date.now() < deadline) { - try { - const r = await gql(downloadQuery); - lastData = r.data?.data?.eSimDownloadToken || null; - if (lastData?.lpaString) { - return { - statusCode: 200, - headers, - body: JSON.stringify({ - success: true, - lpaString: lastData.lpaString, - token: lastData, - ssn: currentSSN, - activationCode: currentActivationCode - }) - }; - } - } catch (e) { - // 忽略短暂错误,继续轮询 - } - await new Promise(r => setTimeout(r, 4000)); - } - - return { statusCode: 202, headers, body: JSON.stringify({ success: false, message: '激活已提交,但暂未获取到LPA,请稍后在“获取eSIM Token”重试。' }) }; - - } catch (error) { - console.error('SMS Activate Error:', { message: error.message }); - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Internal Server Error', message: error.message }) }; +// 输入验证schema +const smsActivateSchema = { + ref: { + required: true, + type: 'string', + minLength: 10 + }, + code: { + required: true, + type: 'string', + minLength: 4, + maxLength: 10 } }; +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 输入验证 + validateInput(smsActivateSchema, body); + + const { + ref, + code, + accessToken, + cookie, + memberId, + ssn, + activationCode + } = body; + + // 站点URL用于内部调用其他函数 + const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); + const host = lower['x-forwarded-host'] || lower['host'] || ''; + const proto = lower['x-forwarded-proto'] || 'https'; + const baseUrl = host ? `${proto}://${host}` : String(process.env.URL || '').replace(/\/$/, ''); + + // 统一创建 GraphQL 客户端 + const createGraphql = (token, extraHeaders = {}) => (body) => axios.post( + 'https://publicapi.giffgaff.com/gateway/graphql', + { ...body, mfaRef: ref }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': token ? `Bearer ${token}` : undefined, + 'Origin': 'https://www.giffgaff.com', + 'Referer': 'https://www.giffgaff.com/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'x-gg-app-os': process.env.GG_APP_OS || 'iOS', + 'x-gg-app-os-version': process.env.GG_APP_OS_VERSION || '18.2', + 'x-gg-app-build-number': process.env.GG_APP_BUILD_NUMBER || '1321', + 'x-gg-app-device-manufacturer': process.env.GG_APP_DEVICE_MANUFACTURER || 'Apple', + 'x-gg-app-device-model': process.env.GG_APP_DEVICE_MODEL || 'iPhone SE', + ...extraHeaders + }, + timeout: 30000 + } + ); + + // 1) 获取 CSRF Token + let csrfToken = null; + if (cookie) { + try { + const csrfResp = await axios.get('https://id.giffgaff.com/auth/csrf', { + headers: { + 'Accept': 'application/json', + 'Cookie': cookie, + 'User-Agent': 'giffgaff/1321 CFNetwork/1568.300.101 Darwin/24.2.0' + }, + timeout: 15000 + }); + csrfToken = csrfResp.data?.token || null; + } catch (e) { + // CSRF获取失败不中断流程 + } + } + + // 2) 校验短信验证码,获取签名 + const buildV4Headers = (token, ck) => ({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'giffgaff/1321 CFNetwork/1568.300.101 Darwin/24.2.0', + 'Origin': 'https://www.giffgaff.com', + 'Referer': 'https://www.giffgaff.com/', + ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + ...(ck ? { 'Cookie': ck } : {}) + }); + + // 从 cookie 中提取 XSRF-TOKEN + let xxsrf = null; + if (cookie) { + const m = String(cookie).match(/XSRF-TOKEN=([^;]+)/); + if (m) xxsrf = decodeURIComponent(m[1]); + } + + const buildV3Headers = (ck) => ({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + 'Origin': 'https://id.giffgaff.com', + 'Referer': 'https://id.giffgaff.com/auth/login/challenge', + 'Device': 'web', + ...(csrfToken ? { 'x-csrf-token': csrfToken } : {}), + ...(xxsrf ? { 'x-xsrf-token': xxsrf } : {}), + ...(ck ? { 'Cookie': ck } : {}) + }); + + let validationResp; + try { + validationResp = await axios.post( + 'https://id.giffgaff.com/v4/mfa/validation', + { ref, code }, + { headers: buildV4Headers(accessToken, cookie), timeout: 30000 } + ); + } catch (err) { + const status = err.response?.status; + const detail = err.response?.data; + const tokenExpired = status === 401; + + if (tokenExpired && cookie) { + try { + // 回退到 Web v3 验证 + validationResp = await axios.post( + 'https://id.giffgaff.com/auth/v3/mfa/validation', + { ref, code }, + { headers: buildV3Headers(cookie), timeout: 30000 } + ); + } catch (err2) { + const s2 = err2.response?.status || 500; + throw new AuthError('MFA Validation Failed', s2); + } + } else { + const s = status || 500; + throw new AuthError('MFA Validation Failed', s); + } + } + + const mfaSignature = validationResp.data?.signature; + if (!mfaSignature) { + throw new AuthError('未获取到MFA签名', 500); + } + + // 3) 若无 ssn/activationCode,则获取 memberId → 预订 eSIM + let currentMemberId = memberId || null; + let currentSSN = ssn || null; + let currentActivationCode = activationCode || null; + + const gql = createGraphql(accessToken); + + if (!currentMemberId) { + const q = { + query: `query getMemberProfileAndSim { memberProfile { id } sim { status } }`, + variables: {}, + operationName: 'getMemberProfileAndSim' + }; + const r = await gql(q); + currentMemberId = r.data?.data?.memberProfile?.id || null; + if (!currentMemberId) { + throw new AuthError('无法获取会员ID', 400); + } + } + + if (!currentSSN || !currentActivationCode) { + const reserveBody = { + query: `mutation reserveESim($input: ESimReservationInput!) { reserveESim: reserveESim(input: $input) { id memberId status esim { ssn activationCode deliveryStatus __typename } __typename } }`, + variables: { input: { memberId: currentMemberId, userIntent: 'SWITCH' } }, + operationName: 'reserveESim' + }; + + const r = await createGraphql(accessToken, { 'X-MFA-Signature': mfaSignature })(reserveBody); + const reservation = r.data?.data?.reserveESim; + if (!reservation?.esim) { + throw new AuthError('eSIM预订失败', 500); + } + currentSSN = reservation.esim.ssn; + currentActivationCode = reservation.esim.activationCode; + } + + // 4) 执行 swapSim + let swapRef = null; + try { + // 先发起 simSwapMfaChallenge + try { + const chBody = { query: `mutation simSwapMfaChallenge { simSwapMfaChallenge { ref methods { value channel __typename } __typename } }`, variables: {}, operationName: 'simSwapMfaChallenge' }; + const ch = await createGraphql(accessToken)({ ...chBody }); + swapRef = ch.data?.data?.simSwapMfaChallenge?.ref || null; + } catch (_) {} + + const swapBody = { + query: `mutation SwapSim($activationCode: String!, $mfaSignature: String!) { swapSim(activationCode: $activationCode, mfaSignature: $mfaSignature) { old { ssn activationCode __typename } new { ssn activationCode __typename } __typename } }`, + variables: { activationCode: currentActivationCode, mfaSignature }, + operationName: 'SwapSim' + }; + const rs = await createGraphql(accessToken)({ ...swapBody, mfaRef: swapRef || ref }); + const sw = rs.data?.data?.swapSim; + if (sw?.new?.ssn) { + currentSSN = sw.new.ssn; + currentActivationCode = sw.new.activationCode || currentActivationCode; + } + } catch (e) { + // 允许继续轮询 + } + + // 5) 轮询获取 LPA + const downloadQuery = { + query: `query eSimDownloadToken($ssn: String!) { eSimDownloadToken(ssn: $ssn) { id host matchingId lpaString __typename } }`, + variables: { ssn: currentSSN }, + operationName: 'eSimDownloadToken' + }; + + const deadline = Date.now() + 120000; + let lastData = null; + while (Date.now() < deadline) { + try { + const r = await gql(downloadQuery); + lastData = r.data?.data?.eSimDownloadToken || null; + if (lastData?.lpaString) { + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + lpaString: lastData.lpaString, + token: lastData, + ssn: currentSSN, + activationCode: currentActivationCode + }) + }; + } + } catch (e) { + // 忽略短暂错误,继续轮询 + } + await new Promise(r => setTimeout(r, 4000)); + } + + return { + statusCode: 202, + body: JSON.stringify({ + success: false, + message: '激活已提交,但暂未获取到LPA,请稍后在"获取eSIM Token"重试。' + }) + }; +}, { validateSchema: smsActivateSchema }); diff --git a/netlify/functions/giffgaff-token-exchange.js b/netlify/functions/giffgaff-token-exchange.js index 3894422..8d09cf3 100644 --- a/netlify/functions/giffgaff-token-exchange.js +++ b/netlify/functions/giffgaff-token-exchange.js @@ -5,177 +5,91 @@ */ const axios = require('axios'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -exports.handler = async (event) => { - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const requestOrigin = lower['origin']; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lower['x-esim-key'] || lower['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; +// 验证环境变量配置(严格验证,不自动修复) +function validateClientCredentials() { + const clientId = process.env.GIFFGAFF_CLIENT_ID; + const clientSecret = process.env.GIFFGAFF_CLIENT_SECRET; - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; + if (!clientId) { + throw new AuthError('GIFFGAFF_CLIENT_ID 未配置', 500); } - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ error: 'Method Not Allowed', message: '只允许POST请求' }) - }; + if (!clientSecret) { + throw new AuthError('GIFFGAFF_CLIENT_SECRET 未配置', 500); } - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; + // 严格验证 clientSecret 格式(Base64) + if (!/^[A-Za-z0-9+/]+=*$/.test(clientSecret)) { + throw new AuthError('GIFFGAFF_CLIENT_SECRET 格式无效:必须为 Base64 编码', 500); } - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; + if (clientSecret.length < 32) { + throw new AuthError('GIFFGAFF_CLIENT_SECRET 长度过短:必须至少32字符', 500); } - try { - const body = JSON.parse(event.body || '{}'); - const { code, code_verifier: codeVerifier, redirect_uri: redirectUri } = body; - - if (!code || !codeVerifier) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ error: 'Bad Request', message: 'code 与 code_verifier 均为必需参数' }) - }; - } - - const clientId = process.env.GIFFGAFF_CLIENT_ID; - const clientSecret = process.env.GIFFGAFF_CLIENT_SECRET; - // 根据Postman配置文件中的设置使用正确的令牌端点URL - const tokenUrl = process.env.GIFFGAFF_TOKEN_URL || 'https://id.giffgaff.com/auth/oauth/token'; - const defaultRedirectUri = process.env.GIFFGAFF_REDIRECT_URI || 'giffgaff://auth/callback/'; - - if (!clientId || !clientSecret) { - return { - statusCode: 500, - headers, - body: JSON.stringify({ - error: 'Server Misconfiguration', - message: '缺少 GIFFGAFF_CLIENT_ID 或 GIFFGAFF_CLIENT_SECRET 环境变量' - }) - }; - } - - // 使用正确的客户端密钥格式,确保包含等号 - let cleanedSecret = clientSecret; - - // 确保客户端密钥包含等号,这在base64密钥中很重要 - if (!cleanedSecret.endsWith('=')) { - // 如果不是以等号结尾,先检查是否以百分号结尾并去除 - if (cleanedSecret.endsWith('%')) { - cleanedSecret = cleanedSecret.slice(0, -1); - console.log('检测到客户端密钥末尾有百分号,已去除'); - } - - // 如果客户端密钥是标准的base64,但缺少等号,添加等号 - if (!/=$/.test(cleanedSecret)) { - cleanedSecret = cleanedSecret + '='; - console.log('客户端密钥可能缺少等号,已添加'); - } - } - - console.log(`使用客户端ID: ${clientId.substring(0, 5)}*****`); - console.log(`客户端密钥长度: ${cleanedSecret.length}`); - const authHeader = Buffer.from(`${clientId}:${cleanedSecret}`).toString('base64'); - - const form = new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: redirectUri || defaultRedirectUri, - code_verifier: codeVerifier - }); - - // 添加调试日志 -console.log(`请求令牌端点: ${tokenUrl}`); -console.log(`请求参数: ${form.toString()}`); -// 不打印敏感信息 -console.log(`请求头部: Authorization: Basic ******, Content-Type: application/x-www-form-urlencoded`); - -// 根据Postman配置使用Authorization头发送客户端凭据,而不是表单参数 -const formWithCredentials = new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: redirectUri || defaultRedirectUri, - code_verifier: codeVerifier -}); - -console.log(`请求参数(包含凭据): ${formWithCredentials.toString().replace(cleanedSecret, '******')}`); - -// 确保授权码没有被额外编码 -// 如果授权码中包含了URL编码字符,尝试解码一次 -let decodedCode = code; -try { - if (code.includes('%')) { - const possiblyDecodedCode = decodeURIComponent(code); - if (possiblyDecodedCode !== code) { - decodedCode = possiblyDecodedCode; - console.log('检测到授权码可能被多次编码,已解码'); - - // 更新表单参数 - formWithCredentials.set('code', decodedCode); - } - } -} catch (e) { - console.log('授权码解码失败,使用原始码'); + return { clientId, clientSecret }; } -const response = await axios.post(tokenUrl, formWithCredentials, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'Authorization': `Basic ${authHeader}` +// 输入验证 schema +const tokenExchangeSchema = { + code: { + required: true, + type: 'string', + minLength: 10, + maxLength: 500 }, - timeout: 30000 -}); - - return { statusCode: 200, headers, body: JSON.stringify(response.data) }; - } catch (error) { - const status = error.response?.status || 500; - const data = error.response?.data || { message: error.message }; - - // 添加详细错误日志 - console.error('Token exchange error:', { - status, - data, - message: error.message, - stack: error.stack - }); - - return { - statusCode: status, - headers, - body: JSON.stringify({ error: 'Token Exchange Failed', details: data }) - }; + code_verifier: { + required: true, + type: 'string', + minLength: 43, + maxLength: 128, + pattern: /^[A-Za-z0-9\-._~]+$/ + }, + redirect_uri: { + required: false, + type: 'string', + maxLength: 500 } }; + +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 输入验证 + validateInput(tokenExchangeSchema, body); + + const { code, code_verifier: codeVerifier, redirect_uri: redirectUri } = body; + + // 验证环境配置 + const { clientId, clientSecret } = validateClientCredentials(); + + const tokenUrl = process.env.GIFFGAFF_TOKEN_URL || 'https://id.giffgaff.com/auth/oauth/token'; + const defaultRedirectUri = process.env.GIFFGAFF_REDIRECT_URI || 'giffgaff://auth/callback/'; + + // 构建 Basic Auth header(不再自动修复密钥) + const authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + const form = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri || defaultRedirectUri, + code_verifier: codeVerifier + }); + + // 执行令牌交换(使用 axios,30秒超时) + const response = await axios.post(tokenUrl, form.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${authHeader}`, + 'User-Agent': 'giffgaff/1332 CFNetwork/1568.300.101 Darwin/24.2.0' + }, + timeout: 30000 + }); + + return { + statusCode: 200, + body: JSON.stringify(response.data) + }; +}, { + validateSchema: tokenExchangeSchema +}); diff --git a/netlify/functions/health.js b/netlify/functions/health.js new file mode 100644 index 0000000..1cb6bcb --- /dev/null +++ b/netlify/functions/health.js @@ -0,0 +1,48 @@ +/** + * Netlify Function: Health Check + * 提供健康检查端点,用于监控服务状态 + */ + +exports.handler = async (event, context) => { + const health = { + status: 'healthy', + timestamp: new Date().toISOString(), + service: 'eSIM-Tools', + version: process.env.APP_VERSION || '2.0.0', + environment: process.env.NODE_ENV || 'production', + uptime: process.uptime(), + checks: { + accessKey: !!process.env.ACCESS_KEY, + giffgaffClientId: !!process.env.GIFFGAFF_CLIENT_ID, + giffgaffClientSecret: !!process.env.GIFFGAFF_CLIENT_SECRET + } + }; + + // 检查关键环境变量 + const missingConfigs = []; + if (!process.env.ACCESS_KEY) { + missingConfigs.push('ACCESS_KEY'); + } + if (!process.env.GIFFGAFF_CLIENT_ID) { + missingConfigs.push('GIFFGAFF_CLIENT_ID'); + } + if (!process.env.GIFFGAFF_CLIENT_SECRET) { + missingConfigs.push('GIFFGAFF_CLIENT_SECRET'); + } + + if (missingConfigs.length > 0) { + health.status = 'degraded'; + health.warnings = missingConfigs.map(key => `${key} not configured`); + } + + const statusCode = health.status === 'healthy' ? 200 : 503; + + return { + statusCode, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate' + }, + body: JSON.stringify(health, null, 2) + }; +}; diff --git a/netlify/functions/verify-cookie.js b/netlify/functions/verify-cookie.js index 95bd3ba..4e3f6e5 100644 --- a/netlify/functions/verify-cookie.js +++ b/netlify/functions/verify-cookie.js @@ -5,378 +5,182 @@ const axios = require('axios'); const cheerio = require('cheerio'); +const { withAuth, validateInput, AuthError } = require('./_shared/middleware'); -// 简单的内存限流(每个函数实例内生效) +// 简单��内存限流(每个函数实例内生效) const RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000; // 5分钟 const RATE_LIMIT_MAX = 15; // 窗口最大次数 const requesterHits = new Map(); // ip -> [timestamps] function isRateLimited(ip) { - const now = Date.now(); - const arr = requesterHits.get(ip) || []; - const recent = arr.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS); - recent.push(now); - requesterHits.set(ip, recent); - return recent.length > RATE_LIMIT_MAX; + const now = Date.now(); + const arr = requesterHits.get(ip) || []; + const recent = arr.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS); + recent.push(now); + requesterHits.set(ip, recent); + return recent.length > RATE_LIMIT_MAX; } -exports.handler = async (event, context) => { - const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://esim.cosr.eu.org'; - const lower = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])); - const requestOrigin = lower['origin']; - const ACCESS_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; - const getProvidedKey = () => { - const fromHeader = lower['x-esim-key'] || lower['x-app-key'] || ''; - if (fromHeader) return fromHeader; - try { - const bodyObj = JSON.parse(event.body || '{}'); - if (bodyObj && typeof bodyObj.authKey === 'string') return bodyObj.authKey; - } catch {} - const q = event.queryStringParameters || {}; - if (q.authKey) return q.authKey; - return ''; - }; - - // 设置CORS头 - const headers = { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Vary': 'Origin', - 'Content-Type': 'application/json' - }; - - // 处理预检请求 - if (event.httpMethod === 'OPTIONS') { - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - return { statusCode: 200, headers, body: '' }; - } - - // 限制来源(服务端内部互调通常无 Origin,将被允许) - if (requestOrigin && requestOrigin !== ALLOWED_ORIGIN) { - return { statusCode: 403, headers, body: JSON.stringify({ error: 'Forbidden', message: 'Origin not allowed' }) }; - } - - if (!ACCESS_KEY) { - return { statusCode: 500, headers, body: JSON.stringify({ success: false, error: 'Server Misconfigured', message: 'ACCESS_KEY not configured' }) }; - } - const provided = getProvidedKey(); - if (!provided || provided !== ACCESS_KEY) { - return { statusCode: 401, headers, body: JSON.stringify({ success: false, error: 'Unauthorized', message: 'Missing or invalid auth key' }) }; - } - - // 只允许POST请求 - if (event.httpMethod !== 'POST') { - return { - statusCode: 405, - headers, - body: JSON.stringify({ - error: 'Method Not Allowed', - message: '只允许POST请求' - }) - }; - } - - try { - // 简单限流 - const ip = (event.headers['x-forwarded-for'] || '').split(',')[0] || event.headers['client-ip'] || event.headers['x-real-ip'] || 'unknown'; - if (isRateLimited(ip)) { - return { - statusCode: 429, - headers, - body: JSON.stringify({ error: 'Too Many Requests', message: '请求过于频繁,请稍后再试' }) - }; - } - - // 解析请求体 - const requestBody = JSON.parse(event.body || '{}'); - const { cookie } = requestBody; - - if (!cookie) { - return { - statusCode: 400, - headers, - body: JSON.stringify({ - error: 'Bad Request', - message: 'Cookie参数不能为空' - }) - }; - } - - console.log('Cookie Validation Request:', { - cookieLength: cookie.length, - timestamp: new Date().toISOString() - }); - - // 验证Cookie并获取Access Token - const result = await validateCookieAndGetToken(cookie); - - if (result.success) { - const looksLikeJwt = typeof result.accessToken === 'string' && result.accessToken.includes('.') && result.accessToken.length > 200; - console.log('Cookie Validation Success:', { - hasAccessToken: !!result.accessToken, - looksLikeJwt, - timestamp: new Date().toISOString() - }); - - // 只有拿到疑似 JWT 的令牌才视为可直接进入第2步 - if (!looksLikeJwt) { - return { - statusCode: 200, - headers, - body: JSON.stringify({ - success: true, - valid: false, - accessToken: null, - memberId: result.memberId || null, - emailSignature: null, - message: 'Cookie验证通过但未获取可用于API的访问令牌,请使用OAuth或确保包含 id.giffgaff.com 域的完整会话后重试' - }) - }; - } - - return { - statusCode: 200, - headers, - body: JSON.stringify({ - success: true, - valid: true, - accessToken: result.accessToken, - memberId: result.memberId || null, - emailSignature: null, - message: 'Cookie验证成功' - }) - }; - } else { - console.log('Cookie Validation Failed:', { - message: result.message, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 401, - headers, - body: JSON.stringify({ - success: false, - valid: false, - error: 'Unauthorized', - message: result.message || 'Cookie验证失败' - }) - }; - } - - } catch (error) { - console.error('Cookie Validation Error:', { - message: error.message, - timestamp: new Date().toISOString() - }); - - return { - statusCode: 500, - headers, - body: JSON.stringify({ - success: false, - error: 'Internal Server Error', - message: '服务器内部错误' - }) - }; - } +// 输入验证schema +const verifyCookieSchema = { + cookie: { + required: true, + type: 'string', + minLength: 10, + maxLength: 8192 + } }; -/** - * 验证Cookie并获取Access Token - */ -async function validateCookieAndGetToken(cookieString) { - try { - // 解析Cookie - const cookies = parseCookie(cookieString); - - if (Object.keys(cookies).length === 0) { - return { - success: false, - message: 'Cookie格式无效' - }; - } +exports.handler = withAuth(async (event, context, { auth, body }) => { + // 输入验证 + validateInput(verifyCookieSchema, body); - // 检查必要的Cookie字段 - const requiredCookies = ['session_token', 'user_id', 'auth_token']; - const foundCookies = {}; - - for (const required of requiredCookies) { - if (cookies[required]) { - foundCookies[required] = cookies[required]; - } - } + // 简单限流 + const ip = (event.headers['x-forwarded-for'] || '').split(',')[0] || + event.headers['client-ip'] || + event.headers['x-real-ip'] || + 'unknown'; - // 如果找不到关键Cookie,尝试其他可能的认证Cookie - if (Object.keys(foundCookies).length === 0) { - for (const [name, value] of Object.entries(cookies)) { - const lowerName = name.toLowerCase(); - if (lowerName.includes('token') || - lowerName.includes('session') || - lowerName.includes('auth')) { - foundCookies[name] = value; - } - } - } + if (isRateLimited(ip)) { + throw new AuthError('��求过于频繁,请稍后再试', 429); + } - if (Object.keys(foundCookies).length === 0) { - return { - success: false, - message: '未找到有效的认证Cookie' - }; - } + const { cookie } = body; - // 尝试使用Cookie调用Giffgaff API验证 - const { accessToken, memberId } = await callGiffgaffAPI(cookies, cookieString); - if (accessToken) { - return { success: true, accessToken, memberId }; - } - return { success: false, message: 'Cookie已过期或无效' }; + // 验证Cookie并获取Access Token + const result = await validateCookieAndGetToken(cookie); - } catch (error) { - console.error('Cookie validation error:', error); - return { - success: false, - message: '验证过程中发生错误' - }; + if (result.success) { + const looksLikeJwt = typeof result.accessToken === 'string' && + result.accessToken.includes('.') && + result.accessToken.length > 200; + + // 只有拿到疑似 JWT 的令牌才视为可直接进入第2步 + if (!looksLikeJwt) { + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + valid: false, + accessToken: null, + memberId: result.memberId || null, + emailSignature: null, + message: 'Cookie验证通过但未获取可用于API的访问令牌,请使用OAuth或确保包含 id.giffgaff.com 域的完整会话后重试' + }) + }; } + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + valid: true, + accessToken: result.accessToken, + memberId: result.memberId || null, + emailSignature: null, + message: 'Cookie验证成功' + }) + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + valid: false, + accessToken: null, + emailSignature: null, + message: result.error || '无法��Cookie获取访问令牌' + }) + }; +}, { + validateSchema: verifyCookieSchema +}); + +/** + * 验证Cookie并尝试获取Access Token + */ +async function validateCookieAndGetToken(cookie) { + try { + // 访问主页,尝试从HTML中提取登录状态或令牌 + const response = await axios.get('https://www.giffgaff.com/', { + headers: { + 'Cookie': cookie, + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' + }, + timeout: 15000 + }); + + const $ = cheerio.load(response.data); + + // 尝试从页面中提取member ID + const memberId = extractMemberId($, response.data); + + // 尝试从Cookie中提取或通过后续请求获取token + const accessToken = await extractAccessToken(cookie); + + return { + success: true, + valid: !!memberId, + memberId, + accessToken + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } } /** - * 解析Cookie字符串 + * 从页面中提取Member ID */ -function parseCookie(cookieString) { - const cookies = {}; - const pairs = cookieString.split(';'); - - for (const pair of pairs) { - const trimmedPair = pair.trim(); - if (!trimmedPair) continue; - - const parts = trimmedPair.split('='); - if (parts.length >= 2) { - const name = parts[0].trim(); - // 重要:值中可能包含 '='(如 Base64/签名),需要合并还原 - const value = parts.slice(1).join('=').trim(); - cookies[name] = value; - } - } - - return cookies; +function extractMemberId($, html) { + // 尝试多种方法提取member ID + const metaMemberId = $('meta[name="member-id"]').attr('content'); + if (metaMemberId) return metaMemberId; + + const dataGgMember = $('[data-gg-member]').attr('data-gg-member'); + if (dataGgMember) return dataGgMember; + + const matchMemberId = html.match(/memberId["\s:]+(\d+)/i); + if (matchMemberId && matchMemberId[1]) return matchMemberId[1]; + + return null; } /** - * 使用Cookie调用Giffgaff API获取Access Token + * 尝试提取或获取Access Token */ -async function callGiffgaffAPI(cookies, rawCookieString) { - try { - // 构建Cookie头 - // 优先使用用户原始 Cookie 串,避免解析/重组导致的字符丢失 - let latestCookies = String(rawCookieString || '').trim(); - if (!latestCookies) { - latestCookies = Object.entries(cookies).map(([name, value]) => `${name}=${value}`).join('; '); - } +async function extractAccessToken(cookie) { + try { + // 尝试访问需要认证的API端点来触发token生成 + const apiResponse = await axios.get('https://www.giffgaff.com/auth/user-status', { + headers: { + 'Cookie': cookie, + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + 'Accept': 'application/json' + }, + timeout: 10000 + }); - // 尝试调用Giffgaff Dashboard验证Cookie(跟踪重定向并解析 Set-Cookie) - const session = axios.create({ maxRedirects: 5, timeout: 30000, validateStatus: () => true }); - let response = await session.get('https://www.giffgaff.com/dashboard', { - headers: { - 'Cookie': latestCookies, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'en-US,en;q=0.5', - 'Referer': 'https://www.giffgaff.com/', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Upgrade-Insecure-Requests': '1', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Dest': 'document', - 'Sec-CH-UA': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', - 'Sec-CH-UA-Mobile': '?0', - 'Sec-CH-UA-Platform': '"Windows"' - } - }); + // 检查响应中是否包含token + const token = apiResponse.data?.accessToken || + apiResponse.data?.token || + apiResponse.headers['x-access-token']; - // 整理所有 Set-Cookie 并合并到 latestCookies - const setCookies = ([]).concat(response.headers['set-cookie'] || []); - if (setCookies.length) { - const merged = mergeSetCookies(latestCookies, setCookies); - latestCookies = merged; - } - - if (response.status === 200 && response.data) { - const html = response.data; - const $ = cheerio.load(html); - // 登录判断:尽量避免误判 - const text = String(html).toLowerCase(); - const containsLogin = /(sign\s*in|log\s*in|login|id\.giffgaff\.com\/auth)/i.test(text); - const containsAccount = /(dashboard|my\s*giffgaff|logout|account|profile|settings)/i.test(text); - const isLoggedIn = containsAccount || (!containsLogin && response.status === 200); - if (isLoggedIn) { - // 尝试从 meta 或脚本中提取 memberId - let memberId = $('meta[name="member-id"]').attr('content') || $('meta[name="giffgaff:member_id"]').attr('content') || null; - if (!memberId) { - const scriptText = $('script').map((_, el) => $(el).html() || '').get().join('\n'); - const m = scriptText.match(/\bmemberId\b["']?\s*[:=]\s*["']([\w-]{6,})["']/i); - if (m) memberId = m[1]; - } - - const tokenLike = findBestTokenFromCookies(cookies); - if (tokenLike) return { accessToken: tokenLike, memberId }; - - const ch = Object.entries(cookies).map(([n, v]) => `${n}=${v}`).join('; '); - const accessToken = Buffer.from(require('crypto').createHash('md5').update(ch).digest('hex')).toString('base64'); - return { accessToken, memberId }; - } - } - - // 非明确登录页也尝试放宽:只要存在会话型 Cookie 即视为可用,并回退生成派生 token - const hasSessionCookie = ['GGUID', 'giffgaff', 'JSESSIONID', 'reese84', 'incap_ses'] - .some((k) => Object.keys(cookies).some((n) => n.toLowerCase().startsWith(k.toLowerCase()))); - if (hasSessionCookie) { - const tokenLike = findBestTokenFromCookies(cookies); - if (tokenLike) return { accessToken: tokenLike, memberId: null }; - const ch = Object.entries(cookies).map(([n, v]) => `${n}=${v}`).join('; '); - const accessToken = Buffer.from(require('crypto').createHash('md5').update(ch).digest('hex')).toString('base64'); - return { accessToken, memberId: null }; - } - - return { accessToken: null, memberId: null }; - - } catch (error) { - console.error('Giffgaff API call error:', error.message); - return { accessToken: null, memberId: null }; - } - -function findBestTokenFromCookies(cookies) { - const candidates = ['GGUID', 'giffgaff', 'JSESSIONID', 'XSRF-TOKEN', 'access_token', 'id_token', 'reese84']; - for (const name of candidates) { - if (cookies[name] && String(cookies[name]).length > 20) return cookies[name]; - } - for (const [name, value] of Object.entries(cookies)) { - const lower = name.toLowerCase(); - if ((/token|session|auth/.test(lower)) && String(value).length > 20) return value; + return token || null; + } catch (error) { + // 尝试从Set-Cookie中提取 + const setCookie = error.response?.headers['set-cookie']; + if (setCookie && Array.isArray(setCookie)) { + const tokenCookie = setCookie.find(c => c.includes('access_token=')); + if (tokenCookie) { + const match = tokenCookie.match(/access_token=([^;]+)/); + return match ? match[1] : null; + } } return null; -} - -function mergeSetCookies(originalCookieHeader, setCookieArray) { - const jar = new Map(); - // 先装入原始 cookie - originalCookieHeader.split(';').map(s => s.trim()).filter(Boolean).forEach(kv => { - const [k, v] = kv.split('='); - if (k && v) jar.set(k.trim(), v.trim()); - }); - // 处理 set-cookie 覆盖 - for (const sc of setCookieArray) { - const pair = String(sc).split(';')[0]; - const [k, v] = pair.split('='); - if (k && typeof v !== 'undefined') jar.set(k.trim(), v.trim()); - } - return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; '); -} + } } diff --git a/package-lock.json b/package-lock.json index 81d18bf..23f7db7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,20 +59,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@apideck/better-ajv-errors": { "version": "0.3.6", "resolved": "https://mirrors.huaweicloud.com/repository/npm/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", @@ -107,9 +93,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -117,23 +103,23 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -149,14 +135,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -196,18 +182,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -263,14 +249,14 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -291,15 +277,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -392,9 +378,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -427,27 +413,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -457,14 +443,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -524,14 +510,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -894,9 +880,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", "dev": true, "license": "MIT", "dependencies": { @@ -927,13 +913,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -944,9 +930,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, "license": "MIT", "dependencies": { @@ -955,7 +941,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -982,14 +968,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1082,9 +1068,9 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", "dev": true, "license": "MIT", "dependencies": { @@ -1181,9 +1167,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", "dev": true, "license": "MIT", "dependencies": { @@ -1247,16 +1233,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1348,9 +1334,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "dev": true, "license": "MIT", "dependencies": { @@ -1358,7 +1344,7 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1401,9 +1387,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1485,9 +1471,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "dev": true, "license": "MIT", "dependencies": { @@ -1534,9 +1520,9 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", - "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1703,21 +1689,21 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1726,42 +1712,42 @@ "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1803,9 +1789,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, "license": "MIT", "engines": { @@ -1828,18 +1814,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -1847,14 +1833,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1878,9 +1864,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.7.1", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -1889,9 +1875,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -1906,9 +1892,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -1923,9 +1909,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -1940,9 +1926,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -1957,9 +1943,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -1974,9 +1960,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -1991,9 +1977,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -2008,9 +1994,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -2025,9 +2011,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -2042,9 +2028,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -2059,9 +2045,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -2076,9 +2062,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -2093,9 +2079,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -2110,9 +2096,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -2127,9 +2113,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -2144,9 +2130,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -2161,9 +2147,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -2178,9 +2164,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -2195,9 +2181,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -2212,9 +2198,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -2229,9 +2215,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -2246,9 +2232,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -2263,9 +2249,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -2280,9 +2266,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -2297,9 +2283,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -2314,9 +2300,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -2330,10 +2316,20 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -2350,13 +2346,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -2373,13 +2369,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -2394,9 +2390,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -2411,9 +2407,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -2428,9 +2424,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -2445,9 +2441,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -2461,10 +2457,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -2479,9 +2492,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -2496,9 +2509,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -2513,9 +2526,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "version": "1.2.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -2530,9 +2543,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -2549,13 +2562,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -2572,13 +2585,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -2595,13 +2608,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -2618,13 +2654,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -2641,13 +2677,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -2664,13 +2700,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -2687,13 +2723,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], @@ -2701,7 +2737,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.4" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2711,9 +2747,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -2731,9 +2767,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -2751,9 +2787,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -3135,6 +3171,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -4164,9 +4211,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "dev": true, "funding": [ { @@ -4184,9 +4231,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4218,9 +4265,9 @@ } }, "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.13.2", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4411,6 +4458,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.30", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://mirrors.huaweicloud.com/repository/npm/basic-auth/-/basic-auth-2.0.1.tgz", @@ -4524,9 +4581,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.0", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -4545,10 +4602,11 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -4665,9 +4723,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001733", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", - "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==", + "version": "1.0.30001756", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", "dev": true, "funding": [ { @@ -4873,20 +4931,6 @@ "dev": true, "license": "MIT" }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-convert/-/color-convert-2.0.1.tgz", @@ -4907,17 +4951,6 @@ "dev": true, "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://mirrors.huaweicloud.com/repository/npm/colord/-/colord-2.9.3.tgz", @@ -5056,9 +5089,9 @@ "license": "MIT" }, "node_modules/core-js": { - "version": "3.45.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/core-js/-/core-js-3.45.0.tgz", - "integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==", + "version": "3.47.0", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5668,9 +5701,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5822,9 +5855,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "version": "1.5.259", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", "dev": true, "license": "ISC" }, @@ -6059,9 +6092,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.12", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6072,32 +6105,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -6605,16 +6638,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -9085,13 +9118,17 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { @@ -9440,16 +9477,16 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "version": "3.1.11", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", "dev": true, "license": "MIT", "dependencies": { @@ -11317,9 +11354,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -11488,16 +11525,16 @@ } }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "version": "0.34.5", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -11506,34 +11543,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/sharp/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -11645,23 +11684,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -12095,13 +12117,17 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.3.0", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/temp-dir": { @@ -12647,9 +12673,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -12776,9 +12802,9 @@ } }, "node_modules/webpack": { - "version": "5.101.0", - "resolved": "https://mirrors.huaweicloud.com/repository/npm/webpack/-/webpack-5.101.0.tgz", - "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", + "version": "5.103.0", + "resolved": "https://mirrors.huaweicloud.com/repository/npm/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", "peer": true, @@ -12791,22 +12817,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.2", + "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { diff --git a/package.json b/package.json index e01dd0d..15f7a2c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "optimize-images": "node scripts/optimize-images.js", "compress": "node scripts/compress.js", "security-check": "node scripts/security-check.js", + "quality-check": "node scripts/quality-check.js", "deploy-prepare": "node scripts/deploy-prepare.js", "deploy-analyze": "node scripts/deploy-analyze.js", "deploy-test": "node scripts/test-deploy-config.js" @@ -37,7 +38,6 @@ "dependencies": { "axios": "^1.12.0", "cheerio": "^1.0.0-rc.12", - "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", diff --git a/scripts/apply-middleware.sh b/scripts/apply-middleware.sh new file mode 100644 index 0000000..25a5c1b --- /dev/null +++ b/scripts/apply-middleware.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# 批量应用中间件到剩余Functions的脚本 +# 使用方法: bash scripts/apply-middleware.sh + +set -e + +FUNCTIONS_DIR="netlify/functions" + +echo "🔧 开始批量重构Functions..." +echo "" + +# 定义需要重构的函数列表(排除已重构的) +FUNCTIONS=( + "giffgaff-graphql" + "giffgaff-mfa-challenge" + "giffgaff-mfa-validation" + "giffgaff-sms-activate" + "auto-activate-esim" +) + +for func in "${FUNCTIONS[@]}"; do + FILE="${FUNCTIONS_DIR}/${func}.js" + + if [ ! -f "$FILE" ]; then + echo "⚠️ 跳过不存在的文件: $FILE" + continue + fi + + echo "📝 处理: $func.js" + + # 备份原文件 + cp "$FILE" "${FILE}.backup" + + # 检查是否已经使用中间件 + if grep -q "withAuth" "$FILE"; then + echo " ✅ 已使用中间件,跳过" + rm "${FILE}.backup" + continue + fi + + echo " ⏳ 添加中间件导入..." + # 在第一个require之后添加中间件导入 + if ! grep -q "_shared/middleware" "$FILE"; then + sed -i.tmp "1,/const.*require/s/\(const.*require.*\);/\1;\nconst { withAuth, validateInput, AuthError } = require('.\/\_shared\/middleware');/" "$FILE" + rm "${FILE}.tmp" 2>/dev/null || true + fi + + echo " ✅ 完成" + echo "" +done + +echo "✨ 批量重构完成!" +echo "" +echo "📋 请手动完成以下步骤:" +echo " 1. 检查每个函数的备份文件(.backup)" +echo " 2. 完成exports.handler重构为withAuth包装" +echo " 3. 添加输入验证schema" +echo " 4. 测试功能是否正常" +echo "" +echo "💡 参考示例: netlify/functions/giffgaff-token-exchange.js" diff --git a/scripts/build-static.js b/scripts/build-static.js index c71bade..0f61c77 100755 --- a/scripts/build-static.js +++ b/scripts/build-static.js @@ -1,4 +1,6 @@ #!/usr/bin/env node +const BuildLogger = require('./logger.js'); + const fs = require('fs'); const path = require('path'); @@ -61,16 +63,16 @@ async function copyDirectory(source, destination) { } (async () => { - console.log('🧹 清理 dist 目录...'); + BuildLogger.log('🧹 清理 dist 目录...'); await removeDist(); await fs.promises.mkdir(distDir, { recursive: true }); for (const entry of entries) { - console.log(`📦 复制 ${entry} -> dist/${entry}`); + BuildLogger.log(`📦 复制 ${entry} -> dist/${entry}`); await copyEntry(entry); } - console.log('✅ 静态资源构建完成,输出目录 dist/'); + BuildLogger.success(' 静态资源构建完成,输出目录 dist/'); })().catch(err => { console.error('构建静态资源失败:', err); process.exitCode = 1; diff --git a/scripts/compress.js b/scripts/compress.js index 81a1573..6812556 100644 --- a/scripts/compress.js +++ b/scripts/compress.js @@ -1,6 +1,8 @@ const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); +const BuildLogger = require('./logger.js'); + const { promisify } = require('util'); const gzip = promisify(zlib.gzip); @@ -83,7 +85,7 @@ async function compressFile(filePath) { brotliSize = brotlied.length; } } catch (error) { - console.log(`Brotli压缩失败 ${fileName}:`, error.message); + BuildLogger.log(`Brotli压缩失败 ${fileName}:`, error.message); } const originalSize = content.length; @@ -141,20 +143,20 @@ async function compressBuild() { return; } - console.log('开始压缩构建文件...'); + BuildLogger.log('开始压缩构建文件...'); try { const results = await compressDirectory(distDir); if (results.length === 0) { - console.log('没有找到需要压缩的文件'); + BuildLogger.log('没有找到需要压缩的文件'); return; } // 显示压缩结果 - console.log('\n压缩结果:'); - console.log('文件名'.padEnd(30) + '原始大小'.padEnd(12) + 'Gzip大小'.padEnd(12) + 'Brotli大小'.padEnd(12) + '压缩率'); - console.log('-'.repeat(80)); + BuildLogger.log('\n压缩结果:'); + BuildLogger.log('文件名'.padEnd(30) + '原始大小'.padEnd(12) + 'Gzip大小'.padEnd(12) + 'Brotli大小'.padEnd(12) + '压缩率'); + BuildLogger.log('-'.repeat(80)); let totalOriginal = 0; let totalGzip = 0; @@ -169,7 +171,7 @@ async function compressBuild() { const gzipKB = (result.gzip / 1024).toFixed(1); const brotliKB = result.brotli ? (result.brotli / 1024).toFixed(1) : '-'; - console.log( + BuildLogger.log( result.file.padEnd(30) + `${originalKB}KB`.padEnd(12) + `${gzipKB}KB`.padEnd(12) + @@ -179,8 +181,8 @@ async function compressBuild() { }); const totalRatio = ((totalOriginal - totalGzip) / totalOriginal * 100).toFixed(1); - console.log('-'.repeat(80)); - console.log( + BuildLogger.log('-'.repeat(80)); + BuildLogger.log( '总计'.padEnd(30) + `${(totalOriginal / 1024).toFixed(1)}KB`.padEnd(12) + `${(totalGzip / 1024).toFixed(1)}KB`.padEnd(12) + @@ -188,8 +190,8 @@ async function compressBuild() { `${totalRatio}%` ); - console.log(`\n压缩完成!共处理 ${results.length} 个文件`); - console.log(`节省空间: ${((totalOriginal - totalGzip) / 1024).toFixed(1)}KB`); + BuildLogger.log(`\n压缩完成!共处理 ${results.length} 个文件`); + BuildLogger.log(`节省空间: ${((totalOriginal - totalGzip) / 1024).toFixed(1)}KB`); } catch (error) { console.error('压缩失败:', error); } diff --git a/scripts/deploy-analyze.js b/scripts/deploy-analyze.js index 1b8d33a..029f1d6 100755 --- a/scripts/deploy-analyze.js +++ b/scripts/deploy-analyze.js @@ -1,4 +1,6 @@ #!/usr/bin/env node +const BuildLogger = require('./logger.js'); + const fs = require('fs'); const path = require('path'); @@ -25,10 +27,10 @@ function listFiles(dir, base = dir) { process.exit(1); } const files = listFiles(distDir); - console.log('📦 dist 构建分析:'); + BuildLogger.log('📦 dist 构建分析:'); files.sort((a, b) => b.size - a.size); files.slice(0, 10).forEach(file => { - console.log(`${file.rel.padEnd(50)} ${(file.size / 1024).toFixed(1)} KB`); + BuildLogger.log(`${file.rel.padEnd(50)} ${(file.size / 1024).toFixed(1)} KB`); }); - console.log(`合计文件 ${files.length} 个,总大小 ${(files.reduce((sum, f) => sum + f.size, 0) / 1024).toFixed(1)} KB`); + BuildLogger.log(`合计文件 ${files.length} 个,总大小 ${(files.reduce((sum, f) => sum + f.size, 0) / 1024).toFixed(1)} KB`); })(); diff --git a/scripts/deploy-prepare.js b/scripts/deploy-prepare.js index 821e300..5b6492f 100755 --- a/scripts/deploy-prepare.js +++ b/scripts/deploy-prepare.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +const BuildLogger = require('./logger.js'); const fs = require('fs'); const path = require('path'); @@ -6,9 +7,9 @@ const projectRoot = path.join(__dirname, '..'); const distDir = path.join(projectRoot, 'dist'); function ensureAccessKey() { - const key = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY; + const key = process.env.ACCESS_KEY; if (!key) { - throw new Error('ACCESS_KEY/ESIM_ACCESS_KEY 未配置,无法保护 Functions。'); + throw new Error('ACCESS_KEY 未配置,无法保护 Functions。'); } } @@ -20,8 +21,8 @@ function ensureDist() { } (function main() { - console.log('🔧 检查部署前置条件...'); + BuildLogger.log('🔧 检查部署前置条件...'); ensureAccessKey(); ensureDist(); - console.log('✅ 部署前检查通过,可继续执行部署流程'); + BuildLogger.success(' 部署前检查通过,可继续执行部署流程'); })(); diff --git a/scripts/logger.js b/scripts/logger.js new file mode 100644 index 0000000..5afa8e7 --- /dev/null +++ b/scripts/logger.js @@ -0,0 +1,90 @@ +/** + * 构建脚本日志工具 + * 为构建/部署脚本提供统一的日志输出 + * 注: 构建脚本始终需要输出信息,因此不像前端Logger那样禁用 + */ + +// ANSI颜色码 +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + gray: '\x1b[90m' +}; + +class BuildLogger { + /** + * 信息日志 (蓝色) + */ + static log(...args) { + console.log(colors.blue + '[INFO]' + colors.reset, ...args); + } + + /** + * 成功日志 (绿色) + */ + static success(...args) { + console.log(colors.green + '[SUCCESS]' + colors.reset, ...args); + } + + /** + * 警告日志 (黄色) + */ + static warn(...args) { + console.warn(colors.yellow + '[WARN]' + colors.reset, ...args); + } + + /** + * 错误日志 (红色) + */ + static error(...args) { + console.error(colors.red + '[ERROR]' + colors.reset, ...args); + } + + /** + * 调试日志 (灰色) + */ + static debug(...args) { + if (process.env.DEBUG || process.env.VERBOSE) { + console.log(colors.gray + '[DEBUG]' + colors.reset, ...args); + } + } + + /** + * 标题日志 (粗体青色) + */ + static title(text) { + console.log('\n' + colors.bold + colors.cyan + text + colors.reset); + console.log(colors.cyan + '='.repeat(text.length) + colors.reset); + } + + /** + * 进度信息 (无标签) + */ + static progress(...args) { + console.log(' ', ...args); + } + + /** + * 检查项 (带emoji) + */ + static check(passed, message) { + const icon = passed ? '✅' : '❌'; + const color = passed ? colors.green : colors.red; + console.log(icon, color + message + colors.reset); + } + + /** + * 步骤开始 + */ + static step(number, total, message) { + console.log(colors.cyan + `\n[${number}/${total}]` + colors.reset, colors.bold + message + colors.reset); + } +} + +module.exports = BuildLogger; diff --git a/scripts/optimize-images.js b/scripts/optimize-images.js index 05c6ab7..1d58511 100644 --- a/scripts/optimize-images.js +++ b/scripts/optimize-images.js @@ -1,6 +1,8 @@ const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); +const BuildLogger = require('./logger.js'); + const { promisify } = require('util'); const readdir = promisify(fs.readdir); @@ -45,7 +47,7 @@ async function optimizeImage(inputPath, outputPath, format, options = {}) { const inputStats = await stat(inputPath); const outputStats = await stat(outputPath); if (outputStats.mtime > inputStats.mtime) { - console.log(`⏭️ 跳过已优化: ${path.basename(outputPath)}`); + BuildLogger.log(`⏭️ 跳过已优化: ${path.basename(outputPath)}`); return true; } } @@ -93,7 +95,7 @@ async function optimizeImage(inputPath, outputPath, format, options = {}) { const outputSize = (await stat(outputPath)).size; const savings = ((inputSize - outputSize) / inputSize * 100).toFixed(1); - console.log(`✅ 优化完成: ${path.basename(inputPath)} -> ${format.toUpperCase()} (节省 ${savings}%)`); + BuildLogger.success(' 优化完成: ${path.basename(inputPath)} -> ${format.toUpperCase()} (节省 ${savings}%)'); return { success: true, inputSize, outputSize, savings }; } catch (error) { console.error(`❌ 优化失败: ${path.basename(inputPath)}`, error.message); @@ -108,7 +110,7 @@ async function generateMultipleFormats(inputPath, filename) { // Check file size threshold const fileStats = await stat(inputPath); if (fileStats.size < config.minFileSize) { - console.log(`⏭️ 跳过小文件: ${filename} (${fileStats.size} bytes)`); + BuildLogger.log(`⏭️ 跳过小文件: ${filename} (${fileStats.size} bytes)`); return []; } @@ -144,7 +146,7 @@ async function generateThumbnails(inputPath, filename) { // Skip if image is already smaller than thumbnail size if (metadata.width <= config.sizes.thumbnail.width && metadata.height <= config.sizes.thumbnail.height) { - console.log(`⏭️ 跳过缩略图生成: ${filename} (已足够小)`); + BuildLogger.log(`⏭️ 跳过缩略图生成: ${filename} (已足够小)`); return { success: true, skipped: true }; } @@ -156,7 +158,7 @@ async function generateThumbnails(inputPath, filename) { const inputStats = await stat(inputPath); const thumbStats = await stat(thumbnailPath); if (thumbStats.mtime > inputStats.mtime) { - console.log(`⏭️ 跳过已存在的缩略图: ${path.basename(thumbnailPath)}`); + BuildLogger.log(`⏭️ 跳过已存在的缩略图: ${path.basename(thumbnailPath)}`); return { success: true, skipped: true }; } } @@ -169,7 +171,7 @@ async function generateThumbnails(inputPath, filename) { .jpeg({ quality: 80, progressive: true }) .toFile(thumbnailPath); - console.log(`✅ 缩略图生成: ${path.basename(thumbnailPath)}`); + BuildLogger.success(' 缩略图生成: ${path.basename(thumbnailPath)}'); return { success: true, skipped: false }; } catch (error) { console.error(`❌ 缩略图生成失败: ${filename}`, error.message); @@ -200,7 +202,7 @@ function generateManifest() { const manifestPath = path.join(config.outputDir, 'manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - console.log(`📋 图片清单已生成: ${manifestPath}`); + BuildLogger.log(`📋 图片清单已生成: ${manifestPath}`); } // Process images with concurrency control @@ -218,7 +220,7 @@ async function processImagesInBatches(imageFiles) { await Promise.all(batch.map(async (filename) => { const inputPath = path.join(config.inputDir, filename); - console.log(`\n🔄 处理: ${filename}`); + BuildLogger.log(`\n🔄 处理: ${filename}`); try { // Generate multiple formats @@ -257,23 +259,23 @@ async function processImagesInBatches(imageFiles) { // 主函数 async function optimizeImages() { - console.log('🚀 开始图片优化...'); - console.log(`📁 输入目录: ${config.inputDir}`); - console.log(`📁 输出目录: ${config.outputDir}`); - console.log(`⚡ 并发数: ${config.maxConcurrent}`); + BuildLogger.log('🚀 开始图片优化...'); + BuildLogger.log(`📁 输入目录: ${config.inputDir}`); + BuildLogger.log(`📁 输出目录: ${config.outputDir}`); + BuildLogger.log(`⚡ 并发数: ${config.maxConcurrent}`); // 确保输出目录存在 ensureOutputDir(); // 检查输入目录是否存在 if (!fs.existsSync(config.inputDir)) { - console.log(`⚠️ 输入目录不存在,创建示例目录: ${config.inputDir}`); + BuildLogger.warn(' 输入目录不存在,创建示例目录: ${config.inputDir}'); fs.mkdirSync(config.inputDir, { recursive: true }); // 创建示例文件 const examplePath = path.join(config.inputDir, 'example.txt'); fs.writeFileSync(examplePath, '请将需要优化的图片文件放在此目录中'); - console.log(`📝 已创建示例文件: ${examplePath}`); + BuildLogger.log(`📝 已创建示例文件: ${examplePath}`); return; } @@ -281,11 +283,11 @@ async function optimizeImages() { const imageFiles = files.filter(isImageFile); if (imageFiles.length === 0) { - console.log('⚠️ 未找到图片文件'); + BuildLogger.warn(' 未找到图片文件'); return; } - console.log(`📸 找到 ${imageFiles.length} 个图片文件`); + BuildLogger.log(`📸 找到 ${imageFiles.length} 个图片文件`); const startTime = Date.now(); const results = await processImagesInBatches(imageFiles); @@ -294,19 +296,19 @@ async function optimizeImages() { // 生成清单 generateManifest(); - console.log(`\n🎉 优化完成!`); - console.log(`⏱️ 用时: ${duration}秒`); - console.log(`✅ 成功: ${results.successful}`); - console.log(`⏭️ 跳过: ${results.skipped}`); - console.log(`❌ 失败: ${results.failed}`); + BuildLogger.log(`\n🎉 优化完成!`); + BuildLogger.log(`⏱️ 用时: ${duration}秒`); + BuildLogger.success(' 成功: ${results.successful}'); + BuildLogger.log(`⏭️ 跳过: ${results.skipped}`); + BuildLogger.error(' 失败: ${results.failed}'); if (results.totalSavings > 0) { const avgSavings = (results.totalSavings / results.successful).toFixed(1); - console.log(`💾 平均节省空间: ${avgSavings}%`); + BuildLogger.log(`💾 平均节省空间: ${avgSavings}%`); } - console.log(`📁 输出目录: ${config.outputDir}`); + BuildLogger.log(`📁 输出目录: ${config.outputDir}`); if (results.failed > 0) { - console.log(`⚠️ 有 ${results.failed} 个文件处理失败`); + BuildLogger.warn(' 有 ${results.failed} 个文件处理失败'); process.exit(1); } } @@ -314,7 +316,7 @@ async function optimizeImages() { // 命令行参数处理 const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { - console.log(` + BuildLogger.log(` 📸 图片优化工具 (Sharp版本) 用法: node optimize-images.js [选项] diff --git a/scripts/quality-check.js b/scripts/quality-check.js new file mode 100644 index 0000000..e89ff9c --- /dev/null +++ b/scripts/quality-check.js @@ -0,0 +1,238 @@ +#!/usr/bin/env node +/** + * 代码质量全面检查脚本 + * 检查所有变更的完整性和代码质量 + */ + +const BuildLogger = require('./logger.js'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const projectRoot = path.join(__dirname, '..'); + +// 检查项配置 +const checks = { + // 1. 语法检查 + syntaxCheck: { + name: '语法检查', + files: [ + 'server.js', + 'webpack.config.js', + 'netlify/functions/_shared/middleware.js', + 'netlify/functions/health.js', + 'netlify/functions/giffgaff-graphql.js', + 'netlify/functions/giffgaff-mfa-challenge.js', + 'netlify/functions/giffgaff-mfa-validation.js', + 'netlify/functions/giffgaff-sms-activate.js', + 'netlify/functions/auto-activate-esim.js', + 'netlify/functions/giffgaff-token-exchange.js', + 'netlify/functions/verify-cookie.js' + ] + }, + + // 2. 环境变量一致性 + envVarCheck: { + name: '环境变量一致性', + required: ['ACCESS_KEY', 'ALLOWED_ORIGIN'], + deprecated: ['ESIM_ACCESS_KEY', 'COOKIE_SECRET'] + }, + + // 3. 依赖完整性 + dependencyCheck: { + name: '依赖完整性', + unused: ['cookie-parser'] + }, + + // 4. 安全配置 + securityCheck: { + name: '安全配置', + patterns: { + weakDefaults: /please_change_me|your-secret-key-here|your-key-here/g, + hardcodedSecrets: /(?:password|secret|key)\s*=\s*['"][^'"]{10,}['"]/gi, + consoleLog: /console\.log\(/g + } + } +}; + +let totalChecks = 0; +let passedChecks = 0; +let failedChecks = 0; + +// 辅助函数 +function checkFile(filePath) { + const fullPath = path.join(projectRoot, filePath); + if (!fs.existsSync(fullPath)) { + BuildLogger.error(`文件不存在: ${filePath}`); + return false; + } + + try { + execSync(`node -c "${fullPath}"`, { stdio: 'pipe' }); + return true; + } catch (error) { + BuildLogger.error(`语法错误: ${filePath}`); + BuildLogger.error(error.message); + return false; + } +} + +function checkEnvExample() { + const envPath = path.join(projectRoot, 'env.example'); + const content = fs.readFileSync(envPath, 'utf8'); + const issues = []; + + // 检查必需变量 + checks.envVarCheck.required.forEach(varName => { + if (!content.includes(`${varName}=`)) { + issues.push(`缺少必需环境变量: ${varName}`); + } + }); + + // 检查废弃变量 + checks.envVarCheck.deprecated.forEach(varName => { + if (content.includes(`${varName}=`)) { + issues.push(`包含废弃环境变量: ${varName}`); + } + }); + + return issues; +} + +function searchInFiles(pattern, files, excludeContext = []) { + const results = []; + files.forEach(file => { + const fullPath = path.join(projectRoot, file); + if (fs.existsSync(fullPath)) { + const content = fs.readFileSync(fullPath, 'utf8'); + const lines = content.split('\n'); + let matchCount = 0; + + lines.forEach((line, index) => { + if (pattern.test(line)) { + // 检查是否在排除的上下文中(如安全检查代码) + const isExcluded = excludeContext.some(ctx => { + const contextLine = lines[index]; + const prevLine = lines[index - 1] || ''; + return contextLine.includes(ctx) || prevLine.includes(ctx); + }); + + if (!isExcluded) { + matchCount++; + } + } + }); + + if (matchCount > 0) { + results.push({ file, matches: matchCount }); + } + } + }); + return results; +} + +function checkPackageJson() { + const pkgPath = path.join(projectRoot, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const issues = []; + + checks.dependencyCheck.unused.forEach(dep => { + if (pkg.dependencies && pkg.dependencies[dep]) { + issues.push(`未使用的依赖: ${dep} (dependencies)`); + } + if (pkg.devDependencies && pkg.devDependencies[dep]) { + issues.push(`未使用的依赖: ${dep} (devDependencies)`); + } + }); + + return issues; +} + +// 执行检查 +function runChecks() { + BuildLogger.title('代码质量全面检查'); + + // 1. 语法检查 + BuildLogger.step(1, 4, checks.syntaxCheck.name); + let syntaxPassed = 0; + checks.syntaxCheck.files.forEach(file => { + totalChecks++; + if (checkFile(file)) { + BuildLogger.check(true, file); + syntaxPassed++; + passedChecks++; + } else { + BuildLogger.check(false, file); + failedChecks++; + } + }); + BuildLogger.progress(`${syntaxPassed}/${checks.syntaxCheck.files.length} 文件通过语法检查\n`); + + // 2. 环境变量检查 + BuildLogger.step(2, 4, checks.envVarCheck.name); + totalChecks++; + const envIssues = checkEnvExample(); + if (envIssues.length === 0) { + BuildLogger.check(true, 'env.example 配置正确'); + passedChecks++; + } else { + BuildLogger.check(false, 'env.example 存在问题:'); + envIssues.forEach(issue => BuildLogger.error(` - ${issue}`)); + failedChecks++; + } + + // 3. 依赖检查 + BuildLogger.step(3, 4, checks.dependencyCheck.name); + totalChecks++; + const depIssues = checkPackageJson(); + if (depIssues.length === 0) { + BuildLogger.check(true, 'package.json 依赖正确'); + passedChecks++; + } else { + BuildLogger.check(false, 'package.json 存在问题:'); + depIssues.forEach(issue => BuildLogger.warn(` - ${issue}`)); + failedChecks++; + } + + // 4. 安全配置检查 + BuildLogger.step(4, 4, checks.securityCheck.name); + + // 检查弱密钥(排除安全检查代码中的引用) + totalChecks++; + const weakDefaults = searchInFiles( + checks.securityCheck.patterns.weakDefaults, + ['env.example', 'netlify/functions/_shared/middleware.js'], + ['if (ACCESS_KEY ===', '安全警告', '警告', '检查'] + ); + if (weakDefaults.length === 0) { + BuildLogger.check(true, '无弱默认配置'); + passedChecks++; + } else { + BuildLogger.check(false, '发现弱默认配置:'); + weakDefaults.forEach(r => BuildLogger.warn(` - ${r.file}: ${r.matches}处`)); + failedChecks++; + } + + // 统计报告 + BuildLogger.title('\n检查结果汇总'); + BuildLogger.log(`总检查项: ${totalChecks}`); + BuildLogger.success(`通过: ${passedChecks}`); + if (failedChecks > 0) { + BuildLogger.error(`失败: ${failedChecks}`); + } + + const successRate = ((passedChecks / totalChecks) * 100).toFixed(1); + BuildLogger.log(`\n通过率: ${successRate}%`); + + if (failedChecks === 0) { + BuildLogger.success('\n✅ 所有检查通过! 代码质量良好。'); + return 0; + } else { + BuildLogger.error('\n❌ 存在质量问题,请修复后重新检查。'); + return 1; + } +} + +// 执行 +const exitCode = runChecks(); +process.exit(exitCode); diff --git a/scripts/replace-console-log.js b/scripts/replace-console-log.js new file mode 100644 index 0000000..4e9f117 --- /dev/null +++ b/scripts/replace-console-log.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/** + * 全局替换console.log为Logger + * 自动在文件开头添加Logger导入,并替换所有console.log调用 + */ + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +// 需要处理的目录 +const DIRS_TO_PROCESS = [ + 'src/js/modules', + 'src/giffgaff/js/modules', + 'src/simyo/js/modules' +]; + +// 需要排除的文件 +const EXCLUDE_FILES = [ + 'src/js/modules/logger.js', // Logger模块本身 + 'src/js/modules/README.md' // 文档文件 +]; + +// 替换console.log为Logger.log +function replaceConsoleLogs(filePath) { + try { + let content = fs.readFileSync(filePath, 'utf8'); + const originalContent = content; + + // 检查是否已经导入了Logger + const hasLoggerImport = /import\s+Logger\s+from/.test(content) || + /const\s+Logger\s*=\s*require/.test(content); + + // 检查是否有console.log需要替换 + const hasConsolelog = /console\.log\s*\(/.test(content); + + if (!hasConsolelog) { + console.log(`⏭️ 跳过 ${filePath} - 无console.log`); + return { replaced: false }; + } + + // 替换console.log为Logger.log + // 保留console.warn和console.error不变 + content = content.replace(/console\.log\s*\(/g, 'Logger.log('); + + // 如果还没有导入Logger,在文件开头添加导入 + if (!hasLoggerImport && hasConsolelog) { + // 计算相对路径 + const fileDir = path.dirname(filePath); + const loggerPath = path.relative(fileDir, 'src/js/modules/logger.js'); + const importPath = loggerPath.startsWith('.') ? loggerPath : `./${loggerPath}`; + + // 添加导入语句 + const importStatement = `import Logger from '${importPath}';\n`; + + // 在第一个import语句后或文件开头添加 + if (/^import\s+/.test(content)) { + // 在最后一个import之后添加 + const lastImportIndex = content.lastIndexOf('\nimport '); + if (lastImportIndex !== -1) { + const nextLineIndex = content.indexOf('\n', lastImportIndex + 1); + content = content.slice(0, nextLineIndex + 1) + importStatement + content.slice(nextLineIndex + 1); + } else { + content = importStatement + content; + } + } else { + // 在文件开头添加 + content = importStatement + '\n' + content; + } + } + + if (content !== originalContent) { + fs.writeFileSync(filePath, content, 'utf8'); + const count = (originalContent.match(/console\.log\s*\(/g) || []).length; + console.log(`✅ ${filePath} - 替换了${count}处console.log`); + return { replaced: true, count }; + } else { + console.log(`⏭️ 跳过 ${filePath} - 无需修改`); + return { replaced: false }; + } + + } catch (error) { + console.error(`❌ 处理 ${filePath} 失败:`, error.message); + return { replaced: false, error: true }; + } +} + +// 主函数 +function main() { + console.log('🚀 开始替换console.log为Logger.log...\n'); + + let totalFiles = 0; + let replacedFiles = 0; + let totalReplacements = 0; + + DIRS_TO_PROCESS.forEach(dir => { + const pattern = path.join(dir, '**/*.js'); + const files = glob.sync(pattern); + + files.forEach(file => { + // 排除特定文件 + if (EXCLUDE_FILES.some(excluded => file.includes(excluded))) { + return; + } + + totalFiles++; + const result = replaceConsoleLogs(file); + if (result.replaced) { + replacedFiles++; + totalReplacements += result.count || 0; + } + }); + }); + + console.log('\n📊 替换统计:'); + console.log(` 总文件数: ${totalFiles}`); + console.log(` 已修改文件: ${replacedFiles}`); + console.log(` console.log替换数: ${totalReplacements}`); + console.log('\n✨ 完成!'); +} + +main(); diff --git a/scripts/security-check.js b/scripts/security-check.js index 3af5d15..d3f8df1 100644 --- a/scripts/security-check.js +++ b/scripts/security-check.js @@ -1,4 +1,6 @@ #!/usr/bin/env node +const BuildLogger = require('./logger.js'); + const fs = require('fs'); const path = require('path'); @@ -89,42 +91,47 @@ function checkDependencies() { // 生成安全报告 function generateSecurityReport() { - console.log('🔒 安全检查报告\n'); + BuildLogger.log('🔒 安全检查报告 +'); const vulnerabilities = checkDependencies(); if (vulnerabilities.length === 0) { - console.log('✅ 未发现已知的安全漏洞'); + BuildLogger.success(' 未发现已知的安全漏洞'); return; } - console.log(`⚠️ 发现 ${vulnerabilities.length} 个潜在安全漏洞:\n`); + BuildLogger.warn(' 发现 ${vulnerabilities.length} 个潜在安全漏洞: +'); vulnerabilities.forEach((vuln, index) => { - console.log(`${index + 1}. ${vuln.package}@${vuln.version}`); - console.log(` 严重程度: ${vuln.severity}`); - console.log(` 描述: ${vuln.description}`); - console.log(` 修复建议: ${vuln.fix}\n`); + BuildLogger.log(`${index + 1}. ${vuln.package}@${vuln.version}`); + BuildLogger.log(` 严重程度: ${vuln.severity}`); + BuildLogger.log(` 描述: ${vuln.description}`); + BuildLogger.log(` 修复建议: ${vuln.fix} +`); }); - console.log('🔧 修复建议:'); - console.log('1. 运行 npm update 更新所有依赖'); - console.log('2. 运行 npm audit fix 自动修复'); - console.log('3. 手动更新特定包到最新版本'); + BuildLogger.log('🔧 修复建议:'); + BuildLogger.log('1. 运行 npm update 更新所有依赖'); + BuildLogger.log('2. 运行 npm audit fix 自动修复'); + BuildLogger.log('3. 手动更新特定包到最新版本'); } // 检查开发环境安全配置 function checkSecurityConfig() { - console.log('\n🔧 安全配置检查:\n'); + BuildLogger.log(' +🔧 安全配置检查: +'); // 检查Helmet配置 const serverPath = path.join(__dirname, '../server.js'); if (fs.existsSync(serverPath)) { const serverContent = fs.readFileSync(serverPath, 'utf8'); if (serverContent.includes('helmet')) { - console.log('✅ Helmet安全头已配置'); + BuildLogger.success(' Helmet安全头已配置'); } else { - console.log('⚠️ 建议添加Helmet安全头'); + BuildLogger.warn(' 建议添加Helmet安全头'); } } @@ -132,9 +139,9 @@ function checkSecurityConfig() { if (fs.existsSync(serverPath)) { const serverContent = fs.readFileSync(serverPath, 'utf8'); if (serverContent.includes('cors')) { - console.log('✅ CORS配置已设置'); + BuildLogger.success(' CORS配置已设置'); } else { - console.log('⚠️ 建议配置CORS'); + BuildLogger.warn(' 建议配置CORS'); } } @@ -150,9 +157,9 @@ function checkSecurityConfig() { if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf8'); if (content.includes('Content-Security-Policy')) { - console.log(`✅ ${file} 已配置CSP`); + BuildLogger.success(' ${file} 已配置CSP'); } else { - console.log(`⚠️ ${file} 建议添加CSP配置`); + BuildLogger.warn(' ${file} 建议添加CSP配置'); } } }); @@ -163,16 +170,17 @@ function main() { generateSecurityReport(); checkSecurityConfig(); - console.log('\n📋 安全最佳实践:'); - console.log('1. 定期更新依赖包'); - console.log('2. 使用npm audit检查安全漏洞'); - console.log('3. 配置适当的安全头'); - console.log('4. 实施内容安全策略(CSP)'); - console.log('5. 使用HTTPS部署'); + BuildLogger.log(' +📋 安全最佳实践:'); + BuildLogger.log('1. 定期更新依赖包'); + BuildLogger.log('2. 使用npm audit检查安全漏洞'); + BuildLogger.log('3. 配置适当的安全头'); + BuildLogger.log('4. 实施内容安全策略(CSP)'); + BuildLogger.log('5. 使用HTTPS部署'); } if (require.main === module) { main(); } -module.exports = { checkDependencies, generateSecurityReport }; \ No newline at end of file +module.exports = { checkDependencies, generateSecurityReport }; diff --git a/scripts/test-deploy-config.js b/scripts/test-deploy-config.js index 9fca0f5..1f64776 100755 --- a/scripts/test-deploy-config.js +++ b/scripts/test-deploy-config.js @@ -1,4 +1,6 @@ #!/usr/bin/env node +const BuildLogger = require('./logger.js'); + const fs = require('fs'); const path = require('path'); @@ -14,5 +16,5 @@ const netlifyToml = path.join(__dirname, '..', 'netlify.toml'); console.error('netlify.toml 未将 publish 指向 dist'); process.exit(1); } - console.log('✅ Netlify 配置检查通过 (publish=dist)'); + BuildLogger.success(' Netlify 配置检查通过 (publish=dist)'); })(); diff --git a/scripts/update-script-logging.js b/scripts/update-script-logging.js new file mode 100644 index 0000000..74344cd --- /dev/null +++ b/scripts/update-script-logging.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +/** + * 更新构建脚本中的console日志为BuildLogger + * 仅替换普通的console.log,保留console.error和console.warn + */ + +const fs = require('fs'); +const path = require('path'); + +// 需要处理的脚本文件 +const SCRIPTS_TO_UPDATE = [ + 'build-static.js', + 'deploy-prepare.js', + 'deploy-analyze.js', + 'test-deploy-config.js', + 'optimize-images.js', + 'compress.js', + 'security-check.js' +]; + +// 需要排除的文件 +const EXCLUDE_FILES = [ + 'logger.js', + 'replace-console-log.js', + 'update-script-logging.js' +]; + +function updateScriptLogging(filePath) { + try { + let content = fs.readFileSync(filePath, 'utf8'); + const originalContent = content; + + // 检查是否已经导入了BuildLogger + const hasLoggerImport = /const\s+(?:BuildLogger|Logger)\s*=\s*require/.test(content); + + // 检查是否有console.log需要替换 + const hasConsoleLog = /console\.log\s*\(/.test(content); + + if (!hasConsoleLog) { + console.log(`⏭️ 跳过 ${path.basename(filePath)} - 无console.log`); + return { replaced: false }; + } + + // 替换console.log为BuildLogger.log + // 识别带emoji的success消息 + content = content.replace(/console\.log\((['"`])✅([^'"`]*)\1\)/g, 'BuildLogger.success(\'$2\')'); + content = content.replace(/console\.log\((['"`])❌([^'"`]*)\1\)/g, 'BuildLogger.error(\'$2\')'); + content = content.replace(/console\.log\((['"`])⚠️([^'"`]*)\1\)/g, 'BuildLogger.warn(\'$2\')'); + content = content.replace(/console\.log\((['"`])🔧([^'"`]*)\1\)/g, 'BuildLogger.log(\'🔧$2\')'); + content = content.replace(/console\.log\((['"`])📊([^'"`]*)\1\)/g, 'BuildLogger.log(\'📊$2\')'); + + // 替换剩余的console.log + content = content.replace(/console\.log\s*\(/g, 'BuildLogger.log('); + + // 如果还没有导入BuildLogger,在文件开头添加导入 + if (!hasLoggerImport && hasConsoleLog) { + // 在第一个require之后或文件开头添加 + const importStatement = `const BuildLogger = require('./logger.js');\n`; + + if (/^const\s+/.test(content)) { + // 在最后一个require之后添加 + const lastRequireMatch = content.match(/const\s+\w+\s*=\s*require\([^)]+\);?/g); + if (lastRequireMatch && lastRequireMatch.length > 0) { + const lastRequire = lastRequireMatch[lastRequireMatch.length - 1]; + const lastRequireIndex = content.lastIndexOf(lastRequire); + const insertIndex = lastRequireIndex + lastRequire.length; + content = content.slice(0, insertIndex) + '\n' + importStatement + content.slice(insertIndex); + } else { + content = importStatement + content; + } + } else { + content = importStatement + '\n' + content; + } + } + + if (content !== originalContent) { + fs.writeFileSync(filePath, content, 'utf8'); + const count = (originalContent.match(/console\.log\s*\(/g) || []).length; + console.log(`✅ ${path.basename(filePath)} - 替换了${count}处console.log`); + return { replaced: true, count }; + } else { + console.log(`⏭️ 跳过 ${path.basename(filePath)} - 无需修改`); + return { replaced: false }; + } + + } catch (error) { + console.error(`❌ 处理 ${path.basename(filePath)} 失败:`, error.message); + return { replaced: false, error: true }; + } +} + +function main() { + console.log('🚀 开始更新构建脚本日志...\n'); + + let totalFiles = 0; + let replacedFiles = 0; + let totalReplacements = 0; + + SCRIPTS_TO_UPDATE.forEach(filename => { + const filePath = path.join(__dirname, filename); + + if (!fs.existsSync(filePath)) { + console.log(`⏭️ 跳过 ${filename} - 文件不存在`); + return; + } + + if (EXCLUDE_FILES.includes(filename)) { + console.log(`⏭️ 跳过 ${filename} - 已排除`); + return; + } + + totalFiles++; + const result = updateScriptLogging(filePath); + if (result.replaced) { + replacedFiles++; + totalReplacements += result.count || 0; + } + }); + + console.log('\n📊 替换统计:'); + console.log(` 总文件数: ${totalFiles}`); + console.log(` 已修改文件: ${replacedFiles}`); + console.log(` console.log替换数: ${totalReplacements}`); + console.log('\n✨ 完成!'); + console.log('\n💡 提示: 构建脚本的日志现在使用带颜色的BuildLogger输出'); +} + +main(); diff --git a/server.js b/server.js index 45bc59f..278b86f 100644 --- a/server.js +++ b/server.js @@ -9,16 +9,17 @@ const path = require('path'); const fs = require('fs'); const helmet = require('helmet'); const morgan = require('morgan'); +const Logger = require('./src/js/modules/logger.js'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; const STATIC_ROOT = path.join(__dirname, process.env.STATIC_ROOT || 'dist'); -const INTERNAL_FUNCTION_KEY = process.env.ACCESS_KEY || process.env.ESIM_ACCESS_KEY || ''; +const INTERNAL_FUNCTION_KEY = process.env.ACCESS_KEY || ''; // 启动时环境检查 if (!INTERNAL_FUNCTION_KEY) { - console.error('❌ ACCESS_KEY 或 ESIM_ACCESS_KEY 未配置'); + console.error('❌ ACCESS_KEY 未配置'); console.error('💡 请在 .env 文件或环境变量中设置 ACCESS_KEY'); console.error('⚠️ Netlify Functions 将无法正常工作,请修复后重启'); } @@ -129,7 +130,7 @@ app.use('/.netlify/functions/giffgaff-sms-activate', wrapNetlifyFunction(giffgaf // Simyo API代理路由 app.use('/api/simyo/*', (req, res) => { const targetUrl = `https://appapi.simyo.nl/simyoapi/api/v1${req.path.replace('/api/simyo', '')}`; - console.log(`[Simyo Proxy] ${req.method} ${req.path} -> ${targetUrl}`); + Logger.log(`[Simyo Proxy] ${req.method} ${req.path} -> ${targetUrl}`); // 设置CORS头(仅允许指定域) res.header('Access-Control-Allow-Origin', ALLOWED_ORIGIN); @@ -220,11 +221,11 @@ app.use((req, res) => { // 启动服务器 app.listen(PORT, () => { - console.log(`🚀 eSIM工具服务器已启动`); - console.log(`📍 本地地址: http://localhost:${PORT}`); - console.log(`🔧 Giffgaff工具: http://localhost:${PORT}/giffgaff`); - console.log(`📱 Simyo工具: http://localhost:${PORT}/simyo`); - console.log(`🌐 环境: ${process.env.NODE_ENV || 'development'}`); + Logger.log(`🚀 eSIM工具服务器已启动`); + Logger.log(`📍 本地地址: http://localhost:${PORT}`); + Logger.log(`🔧 Giffgaff工具: http://localhost:${PORT}/giffgaff`); + Logger.log(`📱 Simyo工具: http://localhost:${PORT}/simyo`); + Logger.log(`🌐 环境: ${process.env.NODE_ENV || 'development'}`); }); module.exports = app; diff --git a/src/giffgaff/js/modules/mfa-handler.js b/src/giffgaff/js/modules/mfa-handler.js index af367f6..6be1c56 100644 --- a/src/giffgaff/js/modules/mfa-handler.js +++ b/src/giffgaff/js/modules/mfa-handler.js @@ -1,3 +1,5 @@ +import Logger from '../../../js/modules/logger.js'; + /** * MFA验证处理模块 * 负责多因素认证流程 @@ -41,7 +43,7 @@ export class MFAHandler { // 检查令牌是否被刷新 if (data._tokenRefreshed && data._newAccessToken) { - console.log(t('giffgaff.mfa.log.tokenRefreshed')); + Logger.log(t('giffgaff.mfa.log.tokenRefreshed')); stateManager.set('accessToken', data._newAccessToken); } @@ -156,7 +158,7 @@ export class MFAHandler { } const responseData = await response.json(); - console.log(t('giffgaff.mfa.log.swapResponse'), responseData); + Logger.log(t('giffgaff.mfa.log.swapResponse'), responseData); if (responseData.errors) { throw new Error(responseData.errors[0].message || t('giffgaff.mfa.errors.genericSendFailed')); diff --git a/src/giffgaff/js/modules/oauth-handler.js b/src/giffgaff/js/modules/oauth-handler.js index aa5fe56..539b317 100644 --- a/src/giffgaff/js/modules/oauth-handler.js +++ b/src/giffgaff/js/modules/oauth-handler.js @@ -1,3 +1,5 @@ +import Logger from '../../../js/modules/logger.js'; + /** * OAuth处理模块 * 负责OAuth 2.0 PKCE认证流程 @@ -81,8 +83,8 @@ export class OAuthHandler { throw new Error(t('giffgaff.oauth.errors.missingCode')); } - console.log(t('giffgaff.oauth.log.codeFound'), code); - console.log(t('giffgaff.oauth.log.stateFound'), state); + Logger.log(t('giffgaff.oauth.log.codeFound'), code); + Logger.log(t('giffgaff.oauth.log.stateFound'), state); // 恢复code verifier let codeVerifier = stateManager.get('codeVerifier'); diff --git a/src/js/main.js b/src/js/main.js index a8c87be..46b667b 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -10,7 +10,7 @@ import { autoInjectFooter } from './modules/footer.js'; // 入口脚本:避免冗余控制台输出 // 通过构建时注入的环境变量设置访问密钥(仅用于本站 Netlify Functions) -window.ESIM_ACCESS_KEY = (typeof process !== 'undefined' && process.env && process.env.ESIM_ACCESS_KEY) ? process.env.ESIM_ACCESS_KEY : ''; +window.ACCESS_KEY = (typeof process !== 'undefined' && process.env && process.env.ACCESS_KEY) ? process.env.ACCESS_KEY : ''; // 注入 Turnstile site key(若存在则在页面加载后自动挂载 Turnstile) // 优先使用构建环境变量;未配置则使用提供的站点密钥 diff --git a/src/js/modules/api-service.js b/src/js/modules/api-service.js index caa01d8..52d48ad 100644 --- a/src/js/modules/api-service.js +++ b/src/js/modules/api-service.js @@ -1,3 +1,5 @@ +import Logger from './logger.js'; + /** * API Service Layer - Provides a consistent interface for API calls * with built-in retry, caching, and error handling @@ -26,14 +28,14 @@ class APIService { if (method === 'GET' && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (Date.now() - cached.timestamp < (options.cacheTime || 300000)) { - console.log(`[API] Cache hit: ${endpoint}`); + Logger.log(`[API] Cache hit: ${endpoint}`); return cached.data; } } // Deduplicate concurrent identical requests if (this.pendingRequests.has(cacheKey)) { - console.log(`[API] Deduplicating request: ${endpoint}`); + Logger.log(`[API] Deduplicating request: ${endpoint}`); return this.pendingRequests.get(cacheKey); } diff --git a/src/js/modules/giffgaff/api.js b/src/js/modules/giffgaff/api.js index ebaf18b..a450e48 100644 --- a/src/js/modules/giffgaff/api.js +++ b/src/js/modules/giffgaff/api.js @@ -1,3 +1,5 @@ +import Logger from '../logger.js'; + /** * Giffgaff API 交互模块 */ @@ -29,7 +31,7 @@ class APIManager { * @returns {Promise} MFA Challenge Response */ async sendMFAChallenge(accessToken) { - console.log('[API] sendMFAChallenge: start', { hasToken: !!accessToken }); + Logger.log('[API] sendMFAChallenge: start', { hasToken: !!accessToken }); // 先检查令牌是否过期,如果过期则尝试使用cookie重新验证 try { // 尝试使用现有令牌 @@ -52,7 +54,7 @@ class APIManager { // 如果响应成功,直接返回结果 if (response.ok) { const json = await response.json(); - console.log('[API] sendMFAChallenge: ok', { hasRef: !!json?.ref }); + Logger.log('[API] sendMFAChallenge: ok', { hasRef: !!json?.ref }); return json; } @@ -68,13 +70,13 @@ class APIManager { // 尝试使用本地存储的cookie重新验证 const cookie = localStorage.getItem('giffgaff_cookie'); if (cookie) { - console.log('尝试使用cookie重新验证'); + Logger.log('尝试使用cookie重新验证'); const cookieVerifyResult = await this.verifyCookie(cookie); if (cookieVerifyResult.success && cookieVerifyResult.accessToken) { // 更新全局令牌 const newAccessToken = cookieVerifyResult.accessToken; - console.log('使用cookie重新验证成功,更新令牌'); + Logger.log('使用cookie重新验证成功,更新令牌'); // 使用新令牌重新发送MFA请求 const newResponse = await fetch(this.endpoints.mfaChallenge, { @@ -126,7 +128,7 @@ class APIManager { * @returns {Promise} MFA Validation Response */ async validateMFACode(accessToken, ref, code) { - console.log('[API] validateMFACode: start', { hasToken: !!accessToken, ref: String(ref||'').slice(0,6)+'...' }); + Logger.log('[API] validateMFACode: start', { hasToken: !!accessToken, ref: String(ref||'').slice(0,6)+'...' }); const response = await fetch(this.endpoints.mfaValidation, { method: 'POST', headers: { @@ -147,7 +149,7 @@ class APIManager { } const json = await response.json(); - console.log('[API] validateMFACode: ok', { hasSignature: !!json?.signature }); + Logger.log('[API] validateMFACode: ok', { hasSignature: !!json?.signature }); return json; } @@ -158,7 +160,7 @@ class APIManager { * - 输出: { success, lpaString, token, ssn, activationCode } */ async smsActivateFlow({ ref, code, accessToken, cookie, memberId, ssn, activationCode }) { - console.log('[API] smsActivateFlow: start', { hasToken: !!accessToken, hasCookie: !!cookie, ref: String(ref||'').slice(0,6)+'...' }); + Logger.log('[API] smsActivateFlow: start', { hasToken: !!accessToken, hasCookie: !!cookie, ref: String(ref||'').slice(0,6)+'...' }); const resp = await fetch(this.endpoints.smsActivate, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) }, @@ -169,7 +171,7 @@ class APIManager { throw new Error(`SMS activate flow failed: ${resp.status} - ${t}`); } const json = await resp.json(); - console.log('[API] smsActivateFlow: ok', { success: !!json?.success, hasLpa: !!json?.lpaString }); + Logger.log('[API] smsActivateFlow: ok', { success: !!json?.success, hasLpa: !!json?.lpaString }); return json; } @@ -184,7 +186,7 @@ class APIManager { async graphqlQuery(accessToken, query, variables = {}, mfaSignature = null) { const opMatch = String(query||'').match(/(query|mutation)\s+(\w+)/i); const op = opMatch ? opMatch[2] : 'Unknown'; - console.log('[API] graphqlQuery: start', { op, hasToken: !!accessToken, hasSignature: !!mfaSignature }); + Logger.log('[API] graphqlQuery: start', { op, hasToken: !!accessToken, hasSignature: !!mfaSignature }); const response = await fetch(this.endpoints.graphql, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, @@ -213,7 +215,7 @@ class APIManager { } const data = await response.json(); - console.log('[API] graphqlQuery: ok', { op, hasErrors: !!data?.errors }); + Logger.log('[API] graphqlQuery: ok', { op, hasErrors: !!data?.errors }); if (data.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`); @@ -229,7 +231,7 @@ class APIManager { * @param {string} redirectUri 可选 */ async exchangeTokenServerSide(code, codeVerifier, redirectUri) { - console.log('[API] tokenExchange: start'); + Logger.log('[API] tokenExchange: start'); const response = await fetch(this.endpoints.tokenExchange, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -240,7 +242,7 @@ class APIManager { throw new Error(`Server token exchange failed: ${response.status} - ${errorText}`); } const json = await response.json(); - console.log('[API] tokenExchange: ok'); + Logger.log('[API] tokenExchange: ok'); return json; } @@ -250,7 +252,7 @@ class APIManager { * @returns {Promise} Member Profile */ async getMemberProfile(accessToken) { - console.log('[API] getMemberProfile: start', { hasToken: !!accessToken }); + Logger.log('[API] getMemberProfile: start', { hasToken: !!accessToken }); const query = ` query getMemberProfileAndSim { memberProfile { @@ -267,7 +269,7 @@ class APIManager { `; const res = await this.graphqlQuery(accessToken, query); - console.log('[API] getMemberProfile: ok', { hasMember: !!res?.data?.memberProfile }); + Logger.log('[API] getMemberProfile: ok', { hasMember: !!res?.data?.memberProfile }); return res; } @@ -279,7 +281,7 @@ class APIManager { * @returns {Promise} Reserve eSIM Response */ async reserveESIM(accessToken, memberId, mfaSignature) { - console.log('[API] reserveESIM: start', { hasToken: !!accessToken, hasSignature: !!mfaSignature }); + Logger.log('[API] reserveESIM: start', { hasToken: !!accessToken, hasSignature: !!mfaSignature }); const query = ` mutation reserveESim($input: ESimReservationInput!) { reserveESim: reserveESim(input: $input) { @@ -315,7 +317,7 @@ class APIManager { localStorage.setItem('gg_esim_ssn', esim.ssn || ''); } } catch (_) {} - console.log('[API] reserveESIM: ok', { hasESIM: !!data?.data?.reserveESim?.esim }); + Logger.log('[API] reserveESIM: ok', { hasESIM: !!data?.data?.reserveESim?.esim }); return data; } @@ -326,7 +328,7 @@ class APIManager { * @returns {Promise} eSIM Token Response */ async getESIMToken(accessToken, ssn) { - console.log('[API] getESIMToken: start', { hasToken: !!accessToken, hasSSN: !!ssn }); + Logger.log('[API] getESIMToken: start', { hasToken: !!accessToken, hasSSN: !!ssn }); const query = ` query eSimDownloadToken($ssn: String!) { eSimDownloadToken(ssn: $ssn) { @@ -348,7 +350,7 @@ class APIManager { const lpa = data?.data?.eSimDownloadToken?.lpaString; if (lpa) localStorage.setItem('gg_esim_lpa', lpa); } catch (_) {} - console.log('[API] getESIMToken: ok', { hasLPA: !!data?.data?.eSimDownloadToken?.lpaString }); + Logger.log('[API] getESIMToken: ok', { hasLPA: !!data?.data?.eSimDownloadToken?.lpaString }); return data; } @@ -358,7 +360,7 @@ class APIManager { * @returns {Promise} Cookie Verification Response */ async verifyCookie(cookie) { - console.log('[API] verifyCookie: start', { hasCookie: !!cookie }); + Logger.log('[API] verifyCookie: start', { hasCookie: !!cookie }); const response = await fetch(this.endpoints.cookieVerify, { method: 'POST', headers: { @@ -376,7 +378,7 @@ class APIManager { } const json = await response.json(); - console.log('[API] verifyCookie: ok', { success: !!json?.success }); + Logger.log('[API] verifyCookie: ok', { success: !!json?.success }); return json; } @@ -386,7 +388,7 @@ class APIManager { * @returns {Promise} Auto Activation Response */ async autoActivateESIM(activationCode) { - console.log('[API] autoActivateESIM: start', { hasCode: !!activationCode }); + Logger.log('[API] autoActivateESIM: start', { hasCode: !!activationCode }); const response = await fetch(this.endpoints.autoActivate, { method: 'POST', headers: { @@ -404,7 +406,7 @@ class APIManager { } const json = await response.json(); - console.log('[API] autoActivateESIM: ok', { success: !!json?.success }); + Logger.log('[API] autoActivateESIM: ok', { success: !!json?.success }); return json; } } diff --git a/src/js/modules/giffgaff/oauth.js b/src/js/modules/giffgaff/oauth.js index 9d7d02f..efce6ad 100644 --- a/src/js/modules/giffgaff/oauth.js +++ b/src/js/modules/giffgaff/oauth.js @@ -1,3 +1,5 @@ +import Logger from '../logger.js'; + /** * Giffgaff OAuth 2.0 PKCE 认证模块 */ @@ -121,7 +123,7 @@ class OAuthManager { */ // 前端不再直接持有 client_secret,改由服务端函数代为交换 async exchangeToken(code, codeVerifier) { - console.log(`Sending token exchange request: code=${code.substring(0, 3)}*****, code_verifier=${codeVerifier.substring(0, 3)}*****`); + Logger.log(`Sending token exchange request: code=${code.substring(0, 3)}*****, code_verifier=${codeVerifier.substring(0, 3)}*****`); // 等待 Turnstile token(最多等待 ~2.5s) let tsToken = this.getTurnstileToken(); for (let i = 0; i < 5 && !tsToken; i++) { diff --git a/src/js/modules/html-sanitizer.js b/src/js/modules/html-sanitizer.js new file mode 100644 index 0000000..c27fee7 --- /dev/null +++ b/src/js/modules/html-sanitizer.js @@ -0,0 +1,195 @@ +/** + * HTML 清理和转义工具模块 + * 防御 XSS 攻击 + */ + +class HTMLSanitizer { + /** + * 转义 HTML 特殊字符 + * @param {string} unsafe - 不安全的字符串 + * @returns {string} 转义后的安全字符串 + */ + static escapeHtml(unsafe) { + if (unsafe === null || unsafe === undefined) return ''; + + return String(unsafe) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); // 额外转义斜杠 + } + + /** + * 转义 HTML 属性值 + * @param {string} value - 属性值 + * @returns {string} 转义后的属性值 + */ + static escapeAttr(value) { + if (value === null || value === undefined) return ''; + + return String(value) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + /** + * 移除所有 HTML 标签 + * @param {string} html - 包含 HTML 的字符串 + * @returns {string} 纯文本 + */ + static stripTags(html) { + if (html === null || html === undefined) return ''; + + const temp = document.createElement('div'); + temp.textContent = html; + return temp.textContent || temp.innerText || ''; + } + + /** + * 清理 HTML,仅保留安全标签 + * @param {string} html - 待清理的 HTML + * @param {string[]} allowedTags - 允许的标签列表 + * @returns {string} 清理后的 HTML + */ + static sanitize(html, allowedTags = ['b', 'i', 'em', 'strong', 'span']) { + if (html === null || html === undefined) return ''; + + const temp = document.createElement('div'); + temp.innerHTML = html; + + // 递归清理节点 + const cleanNode = (node) => { + if (node.nodeType === Node.TEXT_NODE) { + return node; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const tagName = node.tagName.toLowerCase(); + + // 如果不在白名单中,替换为文本内容 + if (!allowedTags.includes(tagName)) { + const textNode = document.createTextNode(node.textContent); + return textNode; + } + + // 移除所有属性(防止 on* 事件处理器) + Array.from(node.attributes).forEach(attr => { + node.removeAttribute(attr.name); + }); + + // 递归清理子节点 + Array.from(node.childNodes).forEach(child => { + const cleaned = cleanNode(child); + if (cleaned !== child) { + node.replaceChild(cleaned, child); + } + }); + + return node; + } + + // 移除注释等其他节点 + return document.createTextNode(''); + }; + + Array.from(temp.childNodes).forEach(child => { + cleanNode(child); + }); + + return temp.innerHTML; + } + + /** + * 安全地设置 innerHTML + * @param {HTMLElement} element - 目标元素 + * @param {string} html - HTML 内容 + * @param {string[]} allowedTags - 允许的标签 + */ + static setInnerHTML(element, html, allowedTags) { + if (!element) { + console.error('[HTMLSanitizer] Invalid element'); + return; + } + + const sanitized = this.sanitize(html, allowedTags); + element.innerHTML = sanitized; + } + + /** + * 创建安全的 DOM 节点 + * @param {string} tag - 标签名 + * @param {Object} attributes - 属性对象 + * @param {string|HTMLElement[]} children - 子元素 + * @returns {HTMLElement} + */ + static createElement(tag, attributes = {}, children = []) { + const element = document.createElement(tag); + + // 设置属性(自动转义) + Object.entries(attributes).forEach(([key, value]) => { + // 禁止设置事件处理器 + if (key.startsWith('on')) { + console.warn(`[HTMLSanitizer] Blocked event handler: ${key}`); + return; + } + + // 转义属性值 + element.setAttribute(key, this.escapeAttr(value)); + }); + + // 添加子元素 + const childArray = Array.isArray(children) ? children : [children]; + childArray.forEach(child => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else if (child instanceof HTMLElement) { + element.appendChild(child); + } + }); + + return element; + } + + /** + * 验证 URL 安全性 + * @param {string} url - 待验证的 URL + * @returns {boolean} 是否安全 + */ + static isSafeURL(url) { + if (!url) return false; + + try { + const parsed = new URL(url, window.location.href); + // 仅允许 http(s) 和 data: 协议 + const safeProtocols = ['http:', 'https:', 'data:']; + return safeProtocols.includes(parsed.protocol); + } catch { + return false; + } + } + + /** + * 清理 URL(移除 javascript: 等危险协议) + * @param {string} url - 待清理的 URL + * @returns {string} 清理后的 URL + */ + static sanitizeURL(url) { + if (!url) return ''; + + // 移除 javascript:, data:text/html 等危险协议 + const dangerousProtocols = /^(javascript|data:text\/html|vbscript):/i; + if (dangerousProtocols.test(url)) { + console.warn('[HTMLSanitizer] Blocked dangerous URL:', url); + return '#'; + } + + return url; + } +} + +export default HTMLSanitizer; diff --git a/src/js/modules/logger.js b/src/js/modules/logger.js new file mode 100644 index 0000000..dfe0cf5 --- /dev/null +++ b/src/js/modules/logger.js @@ -0,0 +1,102 @@ +/** + * 日志工具模块 + * 生产环境禁用console.log,开发环境保留 + */ + +const isDev = typeof process !== 'undefined' ? + process.env.NODE_ENV === 'development' : + (typeof window !== 'undefined' && window.location.hostname === 'localhost'); + +class Logger { + /** + * 信息日志(生产环境禁用) + */ + static log(...args) { + if (isDev) { + console.log('[INFO]', ...args); + } + } + + /** + * 警告日志(生产环境保留) + */ + static warn(...args) { + console.warn('[WARN]', ...args); + } + + /** + * 错误日志(生产环境保留) + */ + static error(...args) { + console.error('[ERROR]', ...args); + } + + /** + * 调试日志(生产环境禁用) + */ + static debug(...args) { + if (isDev) { + console.log('[DEBUG]', ...args); + } + } + + /** + * 敏感数据脱敏记录 + * @param {string} label - 标签 + * @param {string} value - 敏感值 + * @param {number} visibleChars - 可见字符数 + */ + static sensitive(label, value, visibleChars = 5) { + if (!isDev) return; + + if (!value || typeof value !== 'string') { + console.log(`[SENSITIVE] ${label}: (empty)`); + return; + } + + const masked = value.length > visibleChars ? + `${value.substring(0, visibleChars)}${'*'.repeat(Math.min(value.length - visibleChars, 20))}` : + '***'; + + console.log(`[SENSITIVE] ${label}: ${masked} (length: ${value.length})`); + } + + /** + * 性能计时 + * @param {string} label - 计时标签 + */ + static time(label) { + if (isDev) { + console.time(label); + } + } + + /** + * 结束计时 + * @param {string} label - 计时标签 + */ + static timeEnd(label) { + if (isDev) { + console.timeEnd(label); + } + } + + /** + * 表格输出 + * @param {Array|Object} data - 数据 + */ + static table(data) { + if (isDev && console.table) { + console.table(data); + } + } +} + +// 导出 +if (typeof module !== 'undefined' && module.exports) { + module.exports = Logger; +} else if (typeof window !== 'undefined') { + window.Logger = Logger; +} + +export default Logger; diff --git a/src/js/modules/performance-monitor.js b/src/js/modules/performance-monitor.js index 099f8f9..b82f5c0 100644 --- a/src/js/modules/performance-monitor.js +++ b/src/js/modules/performance-monitor.js @@ -1,3 +1,5 @@ +import Logger from './logger.js'; + /** * Performance Monitoring Utility * Tracks and reports Core Web Vitals and custom metrics @@ -208,7 +210,7 @@ class PerformanceMonitor { // Log in development if (process.env.NODE_ENV === 'development') { - console.log(`[Performance] ${name}:`, data); + Logger.log(`[Performance] ${name}:`, data); } // Send to analytics service diff --git a/src/js/modules/secure-storage.js b/src/js/modules/secure-storage.js new file mode 100644 index 0000000..2241866 --- /dev/null +++ b/src/js/modules/secure-storage.js @@ -0,0 +1,161 @@ +import Logger from './logger.js'; + +/** + * 安全存储工具模块 + * 使用 sessionStorage 替代 localStorage,并添加过期时间机制 + * 防御 XSS 攻击窃取敏感数据 + */ + +class SecureStorage { + constructor() { + // 优先使用 sessionStorage (关闭浏览器即清除) + // 若需跨标签页共享,可使用 localStorage,但必须加过期时间 + this.storage = typeof sessionStorage !== 'undefined' ? sessionStorage : null; + this.fallbackStorage = new Map(); // 内存降级 + } + + /** + * 设置带过期时间的数据 + * @param {string} key - 键名 + * @param {any} value - 值 + * @param {number} ttl - 过期时间(毫秒),默认1小时 + */ + setItem(key, value, ttl = 3600000) { + const data = { + value, + expires: Date.now() + ttl, + timestamp: Date.now() + }; + + try { + const serialized = JSON.stringify(data); + if (this.storage) { + this.storage.setItem(key, serialized); + } else { + this.fallbackStorage.set(key, data); + } + } catch (error) { + console.error('[SecureStorage] setItem failed:', error.message); + // 降级到内存存储 + this.fallbackStorage.set(key, data); + } + } + + /** + * 获取数据,自动检查过期时间 + * @param {string} key - 键名 + * @returns {any|null} 未过期的值,或 null + */ + getItem(key) { + try { + let data; + + if (this.storage) { + const serialized = this.storage.getItem(key); + if (!serialized) return null; + data = JSON.parse(serialized); + } else { + data = this.fallbackStorage.get(key); + if (!data) return null; + } + + // 检查过期时间 + if (data.expires && Date.now() > data.expires) { + this.removeItem(key); + return null; + } + + return data.value; + } catch (error) { + console.error('[SecureStorage] getItem failed:', error.message); + return null; + } + } + + /** + * 移除数据 + * @param {string} key - 键名 + */ + removeItem(key) { + try { + if (this.storage) { + this.storage.removeItem(key); + } + this.fallbackStorage.delete(key); + } catch (error) { + console.error('[SecureStorage] removeItem failed:', error.message); + } + } + + /** + * 清除所有数据 + */ + clear() { + try { + if (this.storage) { + this.storage.clear(); + } + this.fallbackStorage.clear(); + } catch (error) { + console.error('[SecureStorage] clear failed:', error.message); + } + } + + /** + * 检查数据是否存在且未过期 + * @param {string} key - 键名 + * @returns {boolean} + */ + has(key) { + return this.getItem(key) !== null; + } + + /** + * 迁移 localStorage 中的旧数据到 sessionStorage + * @param {string} key - 键名 + */ + migrateFromLocalStorage(key) { + if (typeof localStorage === 'undefined') return; + + try { + const oldValue = localStorage.getItem(key); + if (oldValue) { + // 迁移数据(不带过期时间,视为短期数据) + this.setItem(key, oldValue, 1800000); // 30分钟 + // 清除旧数据 + localStorage.removeItem(key); + Logger.log(`[SecureStorage] Migrated ${key} from localStorage`); + } + } catch (error) { + console.error('[SecureStorage] Migration failed:', error.message); + } + } + + /** + * 批量迁移旧数据 + * @param {string[]} keys - 键名数组 + */ + migrateAll(keys) { + keys.forEach(key => this.migrateFromLocalStorage(key)); + } +} + +// 导出单例 +const secureStorage = new SecureStorage(); + +// 自动迁移已知的敏感数据键 +if (typeof window !== 'undefined') { + const SENSITIVE_KEYS = [ + 'giffgaff_cookie', + 'gg_esim_activationCode', + 'gg_esim_lpa', + 'gg_esim_iccid', + 'gg_esim_eid', + 'gg_access_token' + ]; + + // 页面加载时自动迁移 + secureStorage.migrateAll(SENSITIVE_KEYS); +} + +export default secureStorage; diff --git a/webpack.config.js b/webpack.config.js index 100fd90..09be267 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -85,7 +85,7 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin({ - 'process.env.ESIM_ACCESS_KEY': JSON.stringify(process.env.ESIM_ACCESS_KEY || ''), + 'process.env.ACCESS_KEY': JSON.stringify(process.env.ACCESS_KEY || ''), 'process.env.TURNSTILE_SITE_KEY': JSON.stringify(process.env.TURNSTILE_SITE_KEY || ''), }), new CompressionPlugin({