├── out └── .gitkeep ├── .gitignore ├── .npmrc ├── src ├── www │ ├── .well-known │ │ └── acme-challenge │ │ │ └── foo.txt │ └── apisix_acme │ │ └── tool.html ├── index.js ├── apisix │ ├── v3.js │ └── v2.js ├── server.js ├── router.js ├── config.js ├── task.js ├── common.js └── apisix.js ├── .dockerignore ├── Dockerfile ├── package.json ├── .github └── workflows │ └── release.yml ├── config.example.yml ├── LICENSE ├── README.md └── yarn.lock /out/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.local.yml 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com -------------------------------------------------------------------------------- /src/www/.well-known/acme-challenge/foo.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | node_modules 4 | build.sh 5 | config.local.yml 6 | Dockerfile 7 | README.md -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import apisix from './apisix.js' 2 | import common from './common.js' 3 | import config from './config.js' 4 | import server from './server.js' 5 | import task from './task.js' 6 | 7 | async function main() { 8 | try { 9 | common.setupConsole() 10 | config.init() 11 | await server.start() 12 | await apisix.addSelfRoute() 13 | task.scheduleTask() 14 | } catch (err) { 15 | console.error(err.message) 16 | process.exit(1) 17 | } 18 | } 19 | 20 | main() 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM neilpang/acme.sh:3.0.6 3 | 4 | COPY ./ /app/ 5 | 6 | WORKDIR /app 7 | 8 | RUN \ 9 | printf "https://mirrors.cloud.tencent.com/alpine/v3.15/main\nhttps://mirrors.cloud.tencent.com/alpine/v3.15/community\n" > /etc/apk/repositories && \ 10 | apk add -U --no-cache tzdata nodejs npm && \ 11 | npm config set registry https://registry.npmmirror.com && \ 12 | npm install yarn -g && \ 13 | cd /app/ && yarn && \ 14 | npm uninstall yarn -g && \ 15 | rm -rf /usr/local/share/.cache && \ 16 | rm -rf ~/.npm && \ 17 | rm -rf /tmp/* 18 | 19 | EXPOSE 80 20 | 21 | ENTRYPOINT ["node", "src/index.js"] 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apisix-acme", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "dev": "node src/index.js -c config.local.yml" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@koa/router": "^10.1.1", 16 | "axios": "^0.25.0", 17 | "compare-versions": "^6.1.0", 18 | "koa": "^2.13.4", 19 | "koa-body": "^4.2.0", 20 | "moment": "^2.29.1", 21 | "node-schedule": "^2.1.0", 22 | "yaml": "^2.2.1" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "16.18.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.ref }} 16 | 17 | - name: Login to Docker Hub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_TOKEN }} 22 | 23 | - name: Publish Docker 24 | run: | 25 | rm -rf dist 26 | docker build --no-cache --rm --tag tmaize/apisix-acme:${{ github.ref_name }} . 27 | docker push tmaize/apisix-acme:${{ github.ref_name }} 28 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | # 非必填,应用端口,默认80 2 | # port: 80 3 | 4 | # 必填,本应用接口调用token 5 | verify_token: custom_token 6 | 7 | # 必填,apisix admin host 8 | apisix_host: http://apisix:9180 9 | apisix_token: '******' 10 | 11 | # 必填,将自身服务注册到apisix的地址 12 | self_apisix_host: http://apisix-acme:80 13 | 14 | # 必填,证书申请使用,证书快过期时会发送邮件 15 | acme_mail: 'mail@example.com' 16 | 17 | # 非必填,添加自身路由和临时路由的默认优先级 18 | # route_priority: 999 19 | 20 | # 非必填,钉钉token(关键词apisix-acme),用于发送一些通知,可不配置 21 | # ding_ding_token: '' 22 | 23 | # 非必填,自动续期时间,单位天 24 | # renew_day: 30 25 | 26 | # 非必填,定时任务,默认 0 0 1 * * * 27 | # renew_cron: '0 0 1 * * *' 28 | 29 | # 非必填,acme.sh 环境变量设置 30 | # acme_env: 31 | # DOH_USE: 3 32 | 33 | # 非必填,acme.sh 环境参数配置 34 | # acme_param: 35 | # - '--debug 2 --server zerossl' 36 | 37 | # 非必填,单域名(DNS验证,文件验证),泛域名(DNS验证)。只用到单域名证书申请时可不配置 38 | # dns_api: 39 | # - domain: example-1.com 40 | # dns: dns_ali 41 | # env: 42 | # Ali_Key: xxx 43 | # Ali_Secret: xxx 44 | # - domain: example-2.com 45 | # dns: dns_dp 46 | # env: 47 | # DP_Id: xxx 48 | # DP_Key: xxx 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TMaize 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/apisix/v3.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '../config.js' 3 | 4 | /** 5 | * 列出所有证书 6 | * @typedef {{id: string, snis: Array, validity_start: number, validity_end: number}} Item 7 | * @returns {Promise>} 8 | */ 9 | async function sslList() { 10 | const resp = await axios.request({ 11 | method: 'GET', 12 | headers: { 'X-API-KEY': config.apisix_token }, 13 | url: `${config.apisix_host}/apisix/admin/ssls` 14 | }) 15 | 16 | const { data } = resp 17 | const nodes = [] 18 | const results = [] 19 | 20 | if (Array.isArray(data.list)) { 21 | nodes.push(...data.list) 22 | } 23 | 24 | nodes.forEach(node => { 25 | const item = node.value || {} 26 | if (!item.snis) return 27 | 28 | results.push({ 29 | id: item.id, 30 | snis: item.snis, 31 | validity_start: item.validity_start, 32 | validity_end: item.validity_end 33 | }) 34 | }) 35 | 36 | return results 37 | } 38 | 39 | /** 40 | * 设置证书 41 | * @param {string} id 42 | * @param {{snis: Array, cert: string, key: string, validity_start: number, validity_end: number}} data 43 | * @returns {Promise} 44 | */ 45 | async function setupSsl(id, data) { 46 | return axios.request({ 47 | method: 'PUT', 48 | headers: { 'X-API-KEY': config.apisix_token }, 49 | url: `${config.apisix_host}/apisix/admin/ssls/${id}`, 50 | data 51 | }) 52 | } 53 | 54 | export default { 55 | sslList, 56 | setupSsl 57 | } 58 | -------------------------------------------------------------------------------- /src/apisix/v2.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import config from '../config.js' 3 | 4 | /** 5 | * 列出所有证书 6 | * @typedef {{id: string, snis: Array, validity_start: number, validity_end: number}} Item 7 | * @returns {Promise>} 8 | */ 9 | async function sslList() { 10 | const resp = await axios.request({ 11 | method: 'GET', 12 | headers: { 'X-API-KEY': config.apisix_token }, 13 | url: `${config.apisix_host}/apisix/admin/ssl` 14 | }) 15 | 16 | const { data } = resp 17 | const nodes = [] 18 | const results = [] 19 | 20 | if (data.node && data.node.nodes) { 21 | nodes.push(...data.node.nodes) 22 | } 23 | 24 | nodes.forEach(node => { 25 | const item = node.value || {} 26 | if (!item.snis) return 27 | 28 | results.push({ 29 | id: item.id, 30 | snis: item.snis, 31 | validity_start: item.validity_start, 32 | validity_end: item.validity_end 33 | }) 34 | }) 35 | 36 | return results 37 | } 38 | 39 | /** 40 | * 设置证书 41 | * @param {string} id 42 | * @param {{snis: Array, cert: string, key: string, validity_start: number, validity_end: number}} data 43 | * @returns {Promise} 44 | */ 45 | async function setupSsl(id, data) { 46 | return axios.request({ 47 | method: 'PUT', 48 | headers: { 'X-API-KEY': config.apisix_token }, 49 | url: `${config.apisix_host}/apisix/admin/ssl/${id}`, 50 | data 51 | }) 52 | } 53 | 54 | export default { 55 | sslList, 56 | setupSsl 57 | } 58 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import koaBody from 'koa-body' 3 | import common from './common.js' 4 | import config from './config.js' 5 | import router from './router.js' 6 | 7 | const app = new Koa() 8 | 9 | app.use(async (ctx, next) => { 10 | const origin = ctx.header.origin || ctx.origin 11 | ctx.set('Access-Control-Allow-Origin', origin) 12 | ctx.set('Access-Control-Allow-Methods', '*') 13 | ctx.set('Access-Control-Allow-Credentials', 'true') 14 | ctx.set('Access-Control-Expose-Headers', 'Content-Disposition') 15 | ctx.set('Access-Control-Allow-Headers', 'Content-Type,X-CSRF-Token,Authorization,Token,Check-Token') 16 | ctx.set('Access-Control-Max-Age', '60') 17 | 18 | if (ctx.method === 'OPTIONS') { 19 | ctx.status = 204 20 | return 21 | } 22 | 23 | ctx.state.verifyToken = ctx.header['verify-token'] || ctx.header.verify_token || '' 24 | 25 | try { 26 | await next() 27 | } catch (error) { 28 | const message = error.message || error 29 | ctx.body = { code: 500, message } 30 | common.sendMsg(`接口异常: ${message}\n\n` + '```\n' + error.stack + '\n```') 31 | } 32 | }) 33 | 34 | app.use(koaBody({})) 35 | app.use(router.routes()) 36 | 37 | async function start() { 38 | return new Promise(function (resolve, reject) { 39 | const server = app.listen(config.port, '0.0.0.0') 40 | server.on('error', reject) 41 | server.on('listening', function () { 42 | console.log('server start success:', server.address()) 43 | resolve() 44 | }) 45 | }) 46 | } 47 | 48 | export default { 49 | start 50 | } 51 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import KoaRouter from '@koa/router' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import url from 'url' 5 | import config from './config.js' 6 | import task from './task.js' 7 | 8 | const router = new KoaRouter() 9 | 10 | const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url)) 11 | 12 | // 查询任务 13 | router.get('/apisix_acme/task_status', async (ctx, next) => { 14 | if (ctx.state.verifyToken != config.verify_token) { 15 | ctx.body = { code: 401, message: 'invalid VERIFY-TOKEN' } 16 | return 17 | } 18 | 19 | const domain = ctx.query.domain 20 | if (!domain) { 21 | ctx.body = { code: 400, message: 'domain is required' } 22 | return 23 | } 24 | const status = await task.queryTask(domain) 25 | ctx.body = { code: 200, data: status } 26 | }) 27 | 28 | // 创建任务 29 | router.post('/apisix_acme/task_create', async (ctx, next) => { 30 | if (ctx.state.verifyToken != config.verify_token) { 31 | ctx.body = { code: 401, message: 'invalid VERIFY-TOKEN' } 32 | return 33 | } 34 | 35 | const body = ctx.request.body || {} 36 | const domain = body.domain 37 | const serviceList = body.serviceList || [] 38 | const mail = body.mail || config.acme_mail 39 | const force = body.force === true 40 | 41 | if (!domain) { 42 | ctx.body = { code: 400, message: 'domain is required' } 43 | return 44 | } 45 | 46 | const result = await task.createTask(domain, mail, serviceList, force) 47 | ctx.body = result 48 | }) 49 | 50 | // acme text verify 51 | // 主要是处理 /.well-known/acme-challenge/random 这个请求 52 | router.get('(.*)', (ctx, next) => { 53 | let file = ctx.params[0] 54 | 55 | const filePath = path.join(DIR_NAME, 'www', file) 56 | 57 | if (!fs.existsSync(filePath)) { 58 | ctx.status = 404 59 | return 60 | } 61 | 62 | const state = fs.statSync(filePath) 63 | if (state.isDirectory()) { 64 | ctx.status = 404 65 | return 66 | } 67 | 68 | ctx.body = fs.readFileSync(filePath, 'utf8') 69 | }) 70 | 71 | export default router 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apisix-acme 2 | 3 | 管理 apisix 里面的证书,支持自动续期,支持通过接口创建单域名、泛域名证书 4 | 5 | 单域名:优先使用 dns 验证,未配置 dns_api 的情况下使用 apisix 自动创建路由进行文件验证 6 | 7 | 泛域名:只支持使用 dns 验证 8 | 9 | ## 步骤 10 | 11 | 1. 域名解析到 apisix 服务器(dsn 验证不需要该步骤) 12 | 13 | 2. 调用服务 `http://${apisix-server}/apisix_acme/task_create` 创建证书,也可在 apisix 里面手动导入证书 14 | 15 | 3. 每天凌晨会自动检查即将过期且符合格式(单sni)的证书并自动重新申请 16 | 17 | ## 配置文件 18 | 19 | 参考 [config.yml](config.example.yml) 20 | 21 | ## 安装 22 | 23 | 可以直接使用 [tmaize/apisix-acme](https://hub.docker.com/r/tmaize/apisix-acme) 镜像,或者本地构建`./build.sh build` 24 | 25 | ```yaml 26 | version: "3" 27 | 28 | services: 29 | apisix-acme: 30 | image: tmaize/apisix-acme:2.3.4 31 | restart: always 32 | volumes: 33 | - ./out:/app/out 34 | - ./config.yml:/app/config.yml 35 | environment: 36 | - TZ=Asia/Shanghai 37 | networks: 38 | apisix: 39 | 40 | networks: 41 | apisix: 42 | external: true 43 | ``` 44 | 45 | ## API 46 | 47 | - 新增、更新证书 `/apisix_acme/task_create` 48 | 49 | domain 必填,serviceList、mail、force,VERIFY-TOKEN 可选 50 | 51 | ``` 52 | POST {"domain":"example.com","serviceList":[],"mail":"","force":false} 53 | HEADER {VERIFY-TOKEN: xxxxxxxxxx} 54 | ``` 55 | 56 | 响应 57 | 58 | ```json 59 | { "code": 200, "message": "证书已存在且未过期,跳过操作", "data": { "status": "skip", "domain": "example.com" } } 60 | ``` 61 | 62 | ```json 63 | { "code": 200, "message": "证书申请中,等待片刻", "data": { "status": "running", "domain": "example.com" } } 64 | ``` 65 | 66 | ```json 67 | { "code": 200, "message": "任务已提交,等待片刻", "data": { "status": "created", "domain": "example.com" } } 68 | ``` 69 | 70 | - 查询任务 `/apisix_acme/task_status` 71 | 72 | 请求 73 | 74 | ``` 75 | GET ?domain=example.com 76 | ``` 77 | 78 | 响应 79 | 80 | ```json 81 | { "code": 200, "data": { "status": "error", "domain": "example.com", "error": "域名不存在" } } 82 | ``` 83 | 84 | ```json 85 | { "code": 200, "data": { "status": "running", "domain": "example.com" } } 86 | ``` 87 | 88 | ```json 89 | { "code": 200, "data": { "status": "success", "domain": "example.com" } } 90 | ``` 91 | 92 | - 工具页面 `/apisix_acme/tool.html` 93 | 94 | ## Acknowledgments 95 | 96 | [acme.sh](https://github.com/acmesh-official/acme.sh) 97 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import YAML from 'yaml' 4 | 5 | const config = { 6 | init, 7 | port: 80, 8 | verify_token: '', 9 | apisix_host: '', 10 | apisix_token: '', 11 | self_apisix_host: '', 12 | route_priority: 999, 13 | acme_mail: '', 14 | ding_ding_token: '', 15 | renew_day: 0, 16 | renew_cron: '', 17 | renew_less: 0, 18 | dns_api: [], 19 | acme_env: {}, 20 | acme_param: [] 21 | } 22 | 23 | function init() { 24 | let configFile = path.resolve('config.yml') 25 | if (process.argv.indexOf('-c') != -1) { 26 | const idx = process.argv.indexOf('-c') 27 | configFile = path.resolve(process.argv[idx + 1]) 28 | } 29 | 30 | const ok = fs.existsSync(configFile) 31 | if (!ok) throw new Error(`can't find config ${configFile}`) 32 | console.log(`load config ${configFile}`) 33 | 34 | const configText = fs.readFileSync(configFile, 'utf-8') 35 | const f = YAML.parse(configText) 36 | 37 | config.port = Number(f.port) || 80 38 | config.verify_token = String(f.verify_token || '') 39 | config.apisix_host = f.apisix_host || '' 40 | config.apisix_token = f.apisix_token || '' 41 | config.self_apisix_host = f.self_apisix_host || '' 42 | config.acme_mail = f.acme_mail || '' 43 | config.route_priority = f.route_priority === 0 ? 0 : Number(f.route_priority) || 999 44 | config.ding_ding_token = f.ding_ding_token || '' 45 | config.renew_day = Number(f.renew_day) || 30 46 | config.renew_cron = String(f.renew_cron || '0 0 1 * * *') 47 | config.renew_less = config.renew_day * 24 * 60 * 60 48 | config.dns_api = Array.isArray(f.dns_api) ? f.dns_api : [] 49 | config.acme_env = { ...f.acme_env } 50 | config.acme_param = Array.isArray(f.acme_param) ? f.acme_param : [] 51 | 52 | const isConfigServer = config.acme_param.some(item => item.indexOf('--server ') != -1) 53 | if (!isConfigServer) { 54 | config.acme_param.push('--server letsencrypt') 55 | } 56 | 57 | if (config.renew_day <= 0) throw new Error('Bad configure value: renew_day = ' + config.renew_day) 58 | if (!config.verify_token) throw new Error('Need to configure: verify_token') 59 | if (!config.acme_mail) throw new Error('Need to configure: acme_mail') 60 | if (!config.apisix_host) throw new Error('Need to configure: apisix_host') 61 | if (!config.apisix_token) throw new Error('Need to configure: apisix_token') 62 | if (!config.self_apisix_host) throw new Error('Need to configure: self_apisix_host') 63 | if (config.renew_cron.split(' ').length !== 6) throw new Error('Bad configure value: renew_cron = ' + config.renew_cron) 64 | } 65 | 66 | export default config 67 | -------------------------------------------------------------------------------- /src/www/apisix_acme/tool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | APISIX ACME TOOL 8 | 34 | 35 | 36 |
37 |
38 | 42 | 46 | 50 | 54 | 58 | 59 |
60 |
    61 |
  • {{item}}
  • 62 |
63 |
64 | 65 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/task.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import schedule from 'node-schedule' 3 | import apisix from './apisix.js' 4 | import common from './common.js' 5 | import config from './config.js' 6 | 7 | // 运行中 {"status":"running", domain, mail, serviceList, error, force} 8 | // 上次运行失败 {"status":"error", domain, mail, serviceList, error, force} 9 | // 从apisix中查询到数据 {"status":"success", domain, validity_end} 10 | const taskList = [] 11 | let taskLock = false 12 | 13 | async function queryTask(domain) { 14 | const task = taskList.find(item => item.domain === domain) 15 | if (task) return task 16 | 17 | const results = await apisix.listSSL(domain) 18 | if (results.length == 0) return { domain, status: 'error', error: '域名不存在' } 19 | 20 | const validity_end = results.map(item => item.validity_end).sort()[0] // 取最近的一个 21 | return { domain, status: 'success', validity_end } 22 | } 23 | 24 | async function createTask(domain, mail, serviceList, force) { 25 | const task = await queryTask(domain) 26 | 27 | switch (task.status) { 28 | case 'error': 29 | console.log('创建任务', domain) 30 | break 31 | case 'running': 32 | console.log('跳过任务', '已在执行', domain) 33 | return { code: 200, message: '证书申请中,等待片刻', data: { status: 'running', domain } } 34 | case 'success': 35 | const left_seconds = task.validity_end - parseInt(Date.now() / 1000) 36 | const end_date = moment(task.validity_end * 1000).format('YYYY-MM-DD HH:mm:ss') 37 | 38 | const msg_1 = `到期时间:${end_date}` 39 | const msg_2 = `剩余:${common.toFixed(left_seconds / 86400, 2)}天` 40 | const msg_3 = `提前续期:${config.renew_day}天` 41 | 42 | if (left_seconds >= config.renew_less) { 43 | if (force) { 44 | console.log('强制任务', msg_1, msg_2, msg_3, domain) 45 | } else { 46 | console.log('跳过任务', msg_1, msg_2, msg_3, domain) 47 | return { code: 200, message: '证书已存在且未过期,跳过操作', data: { status: 'skip', domain } } 48 | } 49 | } else { 50 | console.log('创建任务', msg_1, msg_2, msg_3, domain) 51 | } 52 | break 53 | default: 54 | break 55 | } 56 | 57 | const idx = taskList.findIndex(item => item.domain === domain) 58 | if (idx != -1) taskList.splice(idx, 1) 59 | taskList.push({ 60 | status: 'running', 61 | domain: domain, 62 | mail, 63 | serviceList, 64 | force, 65 | error: '' 66 | }) 67 | 68 | doTask() 69 | 70 | return { code: 200, message: '任务已提交,等待片刻', data: { status: 'created', domain } } 71 | } 72 | 73 | async function doTask() { 74 | if (taskLock) return 75 | while (true) { 76 | const task = taskList.find(item => item.status !== 'error') 77 | if (!task) { 78 | taskLock = false 79 | return 80 | } 81 | 82 | taskLock = true 83 | const startTime = Date.now() 84 | console.log('执行任务', task.domain) 85 | 86 | const domain = task.domain 87 | const mail = task.mail || config.acme_mail 88 | const serviceList = task.serviceList 89 | const dc = common.getDomainConfig(domain) 90 | 91 | const dnsParam = config.dns_api.find(item => item.domain == dc.rootDomain) 92 | 93 | let isAddRoute = false 94 | try { 95 | if (!dnsParam) { 96 | if (dc.wildcard) { 97 | throw new Error('泛域名证书必须配置 dns_api') 98 | } else { 99 | isAddRoute = true 100 | console.log('添加验证路由', domain) 101 | await apisix.addVerifyRoute(domain) 102 | } 103 | } 104 | let sslInfo = null 105 | if (!task.force) { 106 | sslInfo = await common.createSSLFromCache(domain) 107 | } 108 | if (!sslInfo) { 109 | sslInfo = await common.createSSL(domain, mail, dnsParam, config.acme_env, config.acme_param) 110 | } 111 | 112 | await apisix.applySSL(domain, sslInfo) 113 | 114 | if (Array.isArray(serviceList) && serviceList.length > 0) { 115 | for (let i = 0; i < serviceList.length; i++) { 116 | await apisix.updateServiceHost(serviceList[i], domain, 'add') 117 | } 118 | } 119 | 120 | const idx = taskList.findIndex(item => item == task) 121 | if (idx != -1) taskList.splice(idx, 1) 122 | 123 | const msg_1 = `到期时间: ${moment(sslInfo.validity_end * 1000).format('YYYY-MM-DD HH:mm:ss')}` 124 | const msg_2 = `到期天数: ${parseInt((sslInfo.validity_end - parseInt(Date.now() / 1000)) / 86400)}` 125 | const msg_3 = `任务耗时: ${(Date.now() - startTime) / 1000}秒` 126 | common.sendMsg(`任务成功: ${domain}\n\n${msg_1}\n\n${msg_2}\n\n${msg_3}`) 127 | } catch (err) { 128 | task.status = 'error' 129 | task.error = err.message 130 | console.error('申请失败', domain, err.message, err.stack) 131 | 132 | const detail = err.detail ? `\n\n输出信息: ${err.detail}` : '' 133 | common.sendMsg(`任务失败: ${err.message}\n\n堆栈信息: ${err.stack}` + detail) 134 | } finally { 135 | if (isAddRoute) { 136 | console.log('删除验证路由', domain) 137 | apisix.removeVerifyRoute(domain).catch(() => {}) 138 | } 139 | } 140 | console.log('结束任务', domain) 141 | 142 | await common.sleep(1000) 143 | } 144 | } 145 | 146 | async function renewAll() { 147 | const list = await apisix.listSSL() 148 | for (let i = 0; i < list.length; i++) { 149 | const item = list[i] 150 | await createTask(item.domain, config.acme_mail, [], false).catch(err => {}) 151 | } 152 | } 153 | 154 | async function scheduleTask() { 155 | schedule.scheduleJob('renewAll', config.renew_cron, renewAll) 156 | renewAll() // 立即执行一次 157 | } 158 | 159 | export default { 160 | scheduleTask, 161 | queryTask, 162 | createTask 163 | } 164 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import child_process from 'child_process' 3 | import fs from 'fs' 4 | import moment from 'moment' 5 | import path from 'path' 6 | import url from 'url' 7 | import config from './config.js' 8 | 9 | const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url)) 10 | 11 | /** 12 | * 执行命令 13 | * @param {string} cmd 14 | * @param {import('child_process').SpawnOptionsWithoutStdio} options 15 | * @returns {Promise<{code: number, output: string, error: Error | undefined}>} 16 | */ 17 | async function execShell(cmd, options) { 18 | const arr = cmd.split(' ').filter(item => item != '') 19 | const [bin, ...args] = arr 20 | return new Promise((resolve, reject) => { 21 | const task = child_process.spawn(bin, args, options) 22 | let output = '' 23 | task.on('close', code => { 24 | if (code === 0) { 25 | resolve({ code, output }) 26 | } else { 27 | reject({ code, output, error: new Error(`execShell return ${code}`) }) 28 | } 29 | }) 30 | task.stdout.on('data', buf => { 31 | const str = buf.toString() //iconv.decode(buf, 'gbk') 32 | output += str 33 | process.stdout.write(str) 34 | }) 35 | task.stderr.on('data', buf => { 36 | const str = buf.toString() //iconv.decode(buf, 'gbk') 37 | output += str 38 | process.stderr.write(str) 39 | }) 40 | task.on('error', err => { 41 | reject({ code: '', output, error: err }) 42 | }) 43 | }) 44 | } 45 | 46 | // 解析证书文件 47 | function parseCA(ssl_cer, ssl_key) { 48 | const data = child_process.execSync(`openssl x509 -text -noout -in '${ssl_cer}'`, { encoding: 'utf8' }) 49 | const snis = /DNS:.+/ 50 | .exec(data)[0] 51 | .split(',') 52 | .map(item => item.trim().replace('DNS:', '')) 53 | .filter(item => item != '') 54 | 55 | const start_time = /Not\sBefore.*:\s(.+)/.exec(data)[1].replace('GMT', '').trim() 56 | const end_time = /Not\sAfter.*:\s(.+)/.exec(data)[1].replace('GMT', '').trim() 57 | const validity_start = moment.utc(start_time, 'MMM DD HH:mm:ss YYYY').unix() 58 | const validity_end = moment.utc(end_time, 'MMM DD HH:mm:ss YYYY').unix() 59 | 60 | return { 61 | snis, 62 | cert: fs.readFileSync(ssl_cer, 'utf8'), 63 | key: fs.readFileSync(ssl_key, 'utf8'), 64 | validity_start, 65 | validity_end 66 | } 67 | } 68 | 69 | async function createSSLFromCache(domain) { 70 | const dc = getDomainConfig(domain) 71 | 72 | if (!fs.existsSync(dc.cerPath)) return 73 | 74 | const info = parseCA(dc.cerPath, dc.keyPath) 75 | if (info.validity_end - parseInt(Date.now() / 1000) >= config.renew_less) { 76 | return info 77 | } 78 | 79 | return null 80 | } 81 | 82 | async function createSSL(domain, email, dnsParam, acmeEnv, acmeParam) { 83 | const dc = getDomainConfig(domain) 84 | const options = { timeout: 1000 * 350, env: { ...acmeEnv, ...(dnsParam || {}).env } } 85 | 86 | let argD = `-d ${domain}` 87 | let argM = `-m ${email}` 88 | let argW = '' 89 | let argDNS = '' 90 | 91 | if (dnsParam) { 92 | if (dc.wildcard) { 93 | argD = `-d ${dc.domain} -d ${dc.baseDomain}` 94 | } 95 | argDNS = `--dns ${dnsParam.dns}` 96 | } else { 97 | argW = `-w ${path.join(DIR_NAME, 'www')}` 98 | } 99 | 100 | const args = `--issue --force ${argM} ${argD} ${argDNS} ${argW} ${acmeParam.join(' ')}`.replace(/\s{2,}/, ' ') 101 | console.log('acme.sh 参数', args) 102 | 103 | await execShell(`acme.sh --home /acme.sh ${args}`, options).catch(data => { 104 | return Promise.reject({ 105 | message: 'acme.sh 执行失败', 106 | stack: data.error.stack, 107 | detail: data.output 108 | }) 109 | }) 110 | 111 | fs.mkdirSync(path.dirname(dc.keyPath), { recursive: true }) 112 | 113 | await execShell(`acme.sh --home /acme.sh --install-cert ${argD} --key-file ${dc.keyPath} --fullchain-file ${dc.cerPath}`, { timeout: 1000 * 10 }) 114 | 115 | const info = parseCA(dc.cerPath, dc.keyPath) 116 | 117 | return info 118 | } 119 | 120 | async function sendMsg(text) { 121 | if (!config.ding_ding_token) return 122 | return axios({ 123 | method: 'POST', 124 | url: 'https://oapi.dingtalk.com/robot/send', 125 | params: { access_token: config.ding_ding_token }, 126 | data: { 127 | msgtype: 'markdown', 128 | markdown: { 129 | title: '事件提醒', 130 | text: '## apisix-acme\n\n---\n\n' + text 131 | } 132 | } 133 | }).catch(err => { 134 | console.error('发送消息失败', err.message) 135 | }) 136 | } 137 | 138 | async function sleep(ms) { 139 | return new Promise((resolve, reject) => { 140 | setTimeout(resolve, ms) 141 | }) 142 | } 143 | 144 | function setupConsole() { 145 | const _log = console.log 146 | const _error = console.error 147 | const T_FORMAT = 'YYYY-MM-DD HH:mm:ss' 148 | 149 | console.log = function () { 150 | const t = moment().format(T_FORMAT) 151 | _log.call(this, `${t} I |`, ...arguments) 152 | } 153 | 154 | console.error = function () { 155 | const t = moment().format(T_FORMAT) 156 | _error.call(this, `${t} E |`, ...arguments) 157 | } 158 | } 159 | 160 | function toFixed(n, len, round) { 161 | if (round) { 162 | return Number(n).toFixed(len) 163 | } 164 | const arr = String(n).split('') 165 | if (!arr.includes('.')) { 166 | arr.push('.') 167 | } 168 | arr.push(...new Array(len).fill('0')) 169 | return arr.slice(0, arr.indexOf('.') + len + 1).join('') 170 | } 171 | 172 | /** 173 | * 获取域名基础配置 174 | * @param {string} domain 175 | * @returns {{domain: string, baseDomain: string, rootDomain: string, wildcard: boolean, keyPath: string, cerPath: string}} 176 | */ 177 | function getDomainConfig(domain) { 178 | const wildcard = /^\*\./.test(domain) 179 | const baseDomain = domain.replace(/^\*\./, '') 180 | 181 | let keyPath = path.join('out', `${domain}.key`) 182 | let cerPath = path.join('out', `${domain}.cer`) 183 | if (wildcard) { 184 | keyPath = path.join('out/wildcard', `${baseDomain}.key`) 185 | cerPath = path.join('out/wildcard', `${baseDomain}.cer`) 186 | } 187 | 188 | const list = domain.split('.') 189 | const rootDomain = list.slice(list.length - 2).join('.') 190 | 191 | return { 192 | domain, 193 | baseDomain, 194 | rootDomain, 195 | wildcard, 196 | keyPath, 197 | cerPath 198 | } 199 | } 200 | 201 | export default { 202 | getDomainConfig, 203 | createSSL, 204 | createSSLFromCache, 205 | sendMsg, 206 | sleep, 207 | setupConsole, 208 | toFixed 209 | } 210 | -------------------------------------------------------------------------------- /src/apisix.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { compareVersions } from 'compare-versions' 3 | import v2 from './apisix/v2.js' 4 | import v3 from './apisix/v3.js' 5 | import common from './common.js' 6 | import config from './config.js' 7 | 8 | async function getVersion() { 9 | const headers = await axios 10 | .request({ 11 | method: 'GET', 12 | headers: { 13 | 'X-API-KEY': config.apisix_token 14 | }, 15 | url: `${config.apisix_host}/apisix/admin/routes` 16 | }) 17 | .then(resp => resp.headers) 18 | .catch(err => { 19 | return (err.response || {}).headers || {} 20 | }) 21 | 22 | const server = headers['server'] || '' 23 | const version = server.replace('APISIX/', '') || '0.0.0' 24 | return version 25 | } 26 | 27 | // 把自己注册到 apisix 28 | async function addSelfRoute() { 29 | async function add() { 30 | const id = `apisix_acme` 31 | await axios.request({ 32 | method: 'PUT', 33 | timeout: 5 * 1000, 34 | headers: { 35 | 'X-API-KEY': config.apisix_token 36 | }, 37 | url: `${config.apisix_host}/apisix/admin/routes/${id}`, 38 | data: { 39 | uri: '/apisix_acme/*', 40 | name: id, 41 | methods: ['GET', 'POST', 'OPTIONS', 'HEAD'], 42 | priority: config.route_priority, 43 | // host: domain, 44 | plugins: { 45 | 'proxy-rewrite': { 46 | scheme: 'http' 47 | } 48 | }, 49 | upstream: { 50 | nodes: [ 51 | { 52 | host: new URL(config.self_apisix_host).hostname, 53 | port: Number(new URL(config.self_apisix_host).port) || 80, 54 | weight: 1 55 | } 56 | ], 57 | timeout: { 58 | connect: 100, 59 | send: 100, 60 | read: 100 61 | }, 62 | type: 'roundrobin', 63 | scheme: 'http', 64 | pass_host: 'pass', 65 | keepalive_pool: { 66 | idle_timeout: 60, 67 | requests: 1000, 68 | size: 320 69 | } 70 | }, 71 | status: 1 72 | } 73 | }) 74 | } 75 | 76 | for (let i = 1; i <= 6; i++) { 77 | try { 78 | await add() 79 | break 80 | } catch (error) { 81 | if (i >= 3) console.error('init acme route fail:', error.message || error, 'retrying ...') 82 | if (i == 6) { 83 | common.sendMsg(`init acme route fail: ${error.message || error}`) 84 | return Promise.reject(new Error('init acme route fail: ' + error.message || error)) 85 | } 86 | } 87 | await common.sleep(3000) 88 | } 89 | console.log('init acme route success') 90 | } 91 | 92 | // 添加文件验证路由 93 | async function addVerifyRoute(domain) { 94 | const id = `acme_verify_${domain}` 95 | await axios.request({ 96 | method: 'PUT', 97 | headers: { 98 | 'X-API-KEY': config.apisix_token 99 | }, 100 | url: `${config.apisix_host}/apisix/admin/routes/${id}`, 101 | data: { 102 | uri: '/.well-known/acme-challenge/*', 103 | name: id, 104 | methods: ['GET'], 105 | priority: config.route_priority, 106 | host: domain, 107 | plugins: { 108 | 'proxy-rewrite': { 109 | scheme: 'http' 110 | } 111 | }, 112 | upstream: { 113 | nodes: [ 114 | { 115 | host: new URL(config.self_apisix_host).hostname, 116 | port: Number(new URL(config.self_apisix_host).port) || 80, 117 | weight: 1 118 | } 119 | ], 120 | timeout: { 121 | connect: 6, 122 | send: 6, 123 | read: 6 124 | }, 125 | type: 'roundrobin', 126 | scheme: 'http', 127 | pass_host: 'pass', 128 | keepalive_pool: { 129 | idle_timeout: 60, 130 | requests: 1000, 131 | size: 320 132 | } 133 | }, 134 | status: 1 135 | } 136 | }) 137 | } 138 | 139 | // 删除文件验证路由 140 | async function removeVerifyRoute(domain) { 141 | const id = `acme_verify_${domain}` 142 | await axios.request({ 143 | method: 'DELETE', 144 | headers: { 145 | 'X-API-KEY': config.apisix_token 146 | }, 147 | url: `${config.apisix_host}/apisix/admin/routes/${id}` 148 | }) 149 | } 150 | 151 | /** 152 | * 列出指定单sni的证书,不传列出所有单sni的证书 153 | * @typedef {{id: string, domain: string, validity_start: number, validity_end: number}} Item 154 | * @param {string|undefined} sni 155 | * @returns {Promise>} 156 | */ 157 | async function listSSL(sni) { 158 | const version = await getVersion() 159 | 160 | let list = [] 161 | if (compareVersions(version, '3.0.0') >= 0) { 162 | list = await v3.sslList() 163 | } else { 164 | list = await v2.sslList() 165 | } 166 | 167 | const results = [] 168 | 169 | list.forEach(item => { 170 | if (!Array.isArray(item.snis) || item.snis.length == 0) { 171 | return 172 | } 173 | 174 | const isSingle = item.snis.length == 1 175 | 176 | let isWildcard = false 177 | const idx1 = item.snis.findIndex(d => /^\*\./.test(d)) 178 | if (idx1 != -1 && item.snis.length === 2) { 179 | const idx2 = idx1 === 1 ? 0 : 1 180 | isWildcard = '*.' + item.snis[idx2] === item.snis[idx1] 181 | } 182 | 183 | if (isSingle || isWildcard) { 184 | const domain = isSingle ? item.snis[0] : item.snis[idx1] 185 | if (sni && domain !== sni) { 186 | return 187 | } 188 | results.push({ 189 | id: item.id, 190 | domain, 191 | validity_start: item.validity_start, 192 | validity_end: item.validity_end 193 | }) 194 | } 195 | }) 196 | 197 | return results 198 | } 199 | 200 | // 导入证书 201 | async function applySSL(domain, sslInfo) { 202 | const sslList = await listSSL(domain) 203 | 204 | const idList = [] 205 | sslList.forEach(item => { 206 | if (item.validity_end < sslInfo.validity_end) { 207 | idList.push(item.id) 208 | } 209 | }) 210 | 211 | if (idList.length == 0) { 212 | idList.push(String(Date.now())) 213 | } 214 | 215 | const version = await getVersion() 216 | 217 | for (let i = 0; i < idList.length; i++) { 218 | const id = idList[i] 219 | if (compareVersions(version, '3.0.0') >= 0) { 220 | await v3.setupSsl(id, sslInfo) 221 | } else { 222 | await v2.setupSsl(id, sslInfo) 223 | } 224 | } 225 | } 226 | 227 | // 把 host 加到某个 service 下 228 | async function updateServiceHost(serviceName, domain, type) { 229 | const resp1 = await axios.request({ 230 | method: 'GET', 231 | headers: { 232 | 'X-API-KEY': config.apisix_token 233 | }, 234 | url: `${config.apisix_host}/apisix/admin/services` 235 | }) 236 | 237 | const nodes = resp1.data.node.nodes || [] 238 | let service 239 | for (let i = 0; i < nodes.length; i++) { 240 | const value = nodes[i].value || {} 241 | if (String(value.id) === serviceName || value.name === serviceName) { 242 | service = value 243 | } 244 | } 245 | 246 | if (!service) { 247 | throw new Error(`service ${serviceName} not found`) 248 | } 249 | 250 | const hosts_old = service.hosts || [] 251 | const hosts_new = hosts_old.slice(0) 252 | 253 | if (type === 'add' && hosts_new.indexOf(domain) === -1) { 254 | console.log('服务绑定域名', serviceName, domain) 255 | hosts_new.push(domain) 256 | } 257 | if (type === 'remove' && hosts_old.indexOf(domain) !== -1) { 258 | console.log('服务移除域名', serviceName, domain) 259 | hosts_new.splice(hosts_new.indexOf(domain), 1) 260 | } 261 | 262 | if (hosts_new.length === hosts_old.length) { 263 | return 264 | } 265 | 266 | await axios.request({ 267 | method: 'PATCH', 268 | headers: { 269 | 'X-API-KEY': config.apisix_token 270 | }, 271 | url: `${config.apisix_host}/apisix/admin/services/${service.id}`, 272 | data: { 273 | hosts: hosts_new 274 | } 275 | }) 276 | } 277 | 278 | export default { 279 | addSelfRoute, 280 | addVerifyRoute, 281 | removeVerifyRoute, 282 | listSSL, 283 | applySSL, 284 | updateServiceHost 285 | } 286 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@koa/router@^10.1.1": 6 | version "10.1.1" 7 | resolved "https://registry.npmmirror.com/@koa/router/-/router-10.1.1.tgz#8e5a85c9b243e0bc776802c0de564561e57a5f78" 8 | integrity sha512-ORNjq5z4EmQPriKbR0ER3k4Gh7YGNhWDL7JBW+8wXDrHLbWYKYSJaOJ9aN06npF5tbTxe2JBOsurpJDAvjiXKw== 9 | dependencies: 10 | debug "^4.1.1" 11 | http-errors "^1.7.3" 12 | koa-compose "^4.1.0" 13 | methods "^1.1.2" 14 | path-to-regexp "^6.1.0" 15 | 16 | "@types/formidable@^1.0.31": 17 | version "1.2.6" 18 | resolved "https://registry.npmmirror.com/@types/formidable/-/formidable-1.2.6.tgz#eee56eca2bd7108c6f1e4dfc9ef10493b9c16543" 19 | integrity sha512-9xwITWH5ok4MrALa7qnUd3McKrvEn5iUZM5/m0AJjOo/sMPUISzuBK/qAHHMV9t5ShjG4fjr0VEm8J+szAKDWA== 20 | dependencies: 21 | "@types/node" "*" 22 | 23 | "@types/node@*": 24 | version "20.6.3" 25 | resolved "https://registry.npmmirror.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9" 26 | integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA== 27 | 28 | "@types/node@16.18.3": 29 | version "16.18.3" 30 | resolved "https://registry.npmmirror.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" 31 | integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== 32 | 33 | accepts@^1.3.5: 34 | version "1.3.8" 35 | resolved "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 36 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 37 | dependencies: 38 | mime-types "~2.1.34" 39 | negotiator "0.6.3" 40 | 41 | axios@^0.25.0: 42 | version "0.25.0" 43 | resolved "https://registry.npmmirror.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" 44 | integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== 45 | dependencies: 46 | follow-redirects "^1.14.7" 47 | 48 | bytes@3.1.2: 49 | version "3.1.2" 50 | resolved "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 51 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 52 | 53 | cache-content-type@^1.0.0: 54 | version "1.0.1" 55 | resolved "https://registry.npmmirror.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" 56 | integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== 57 | dependencies: 58 | mime-types "^2.1.18" 59 | ylru "^1.2.0" 60 | 61 | call-bind@^1.0.0: 62 | version "1.0.2" 63 | resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 64 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 65 | dependencies: 66 | function-bind "^1.1.1" 67 | get-intrinsic "^1.0.2" 68 | 69 | co-body@^5.1.1: 70 | version "5.2.0" 71 | resolved "https://registry.npmmirror.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124" 72 | integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ== 73 | dependencies: 74 | inflation "^2.0.0" 75 | qs "^6.4.0" 76 | raw-body "^2.2.0" 77 | type-is "^1.6.14" 78 | 79 | co@^4.6.0: 80 | version "4.6.0" 81 | resolved "https://registry.npmmirror.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 82 | integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== 83 | 84 | compare-versions@^6.1.0: 85 | version "6.1.0" 86 | resolved "https://registry.npmmirror.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" 87 | integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== 88 | 89 | content-disposition@~0.5.2: 90 | version "0.5.4" 91 | resolved "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 92 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 93 | dependencies: 94 | safe-buffer "5.2.1" 95 | 96 | content-type@^1.0.4: 97 | version "1.0.5" 98 | resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 99 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 100 | 101 | cookies@~0.8.0: 102 | version "0.8.0" 103 | resolved "https://registry.npmmirror.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" 104 | integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== 105 | dependencies: 106 | depd "~2.0.0" 107 | keygrip "~1.1.0" 108 | 109 | cron-parser@^4.2.0: 110 | version "4.9.0" 111 | resolved "https://registry.npmmirror.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" 112 | integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== 113 | dependencies: 114 | luxon "^3.2.1" 115 | 116 | debug@^4.1.1, debug@^4.3.2: 117 | version "4.3.4" 118 | resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 119 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 120 | dependencies: 121 | ms "2.1.2" 122 | 123 | deep-equal@~1.0.1: 124 | version "1.0.1" 125 | resolved "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 126 | integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== 127 | 128 | delegates@^1.0.0: 129 | version "1.0.0" 130 | resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 131 | integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== 132 | 133 | depd@2.0.0, depd@^2.0.0, depd@~2.0.0: 134 | version "2.0.0" 135 | resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 136 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 137 | 138 | depd@~1.1.2: 139 | version "1.1.2" 140 | resolved "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 141 | integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== 142 | 143 | destroy@^1.0.4: 144 | version "1.2.0" 145 | resolved "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 146 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 147 | 148 | ee-first@1.1.1: 149 | version "1.1.1" 150 | resolved "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 151 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 152 | 153 | encodeurl@^1.0.2: 154 | version "1.0.2" 155 | resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 156 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 157 | 158 | escape-html@^1.0.3: 159 | version "1.0.3" 160 | resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 161 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 162 | 163 | follow-redirects@^1.14.7: 164 | version "1.15.3" 165 | resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" 166 | integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== 167 | 168 | formidable@^1.1.1: 169 | version "1.2.6" 170 | resolved "https://registry.npmmirror.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" 171 | integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== 172 | 173 | fresh@~0.5.2: 174 | version "0.5.2" 175 | resolved "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 176 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 177 | 178 | function-bind@^1.1.1: 179 | version "1.1.1" 180 | resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 181 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 182 | 183 | get-intrinsic@^1.0.2: 184 | version "1.2.1" 185 | resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 186 | integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== 187 | dependencies: 188 | function-bind "^1.1.1" 189 | has "^1.0.3" 190 | has-proto "^1.0.1" 191 | has-symbols "^1.0.3" 192 | 193 | has-proto@^1.0.1: 194 | version "1.0.1" 195 | resolved "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" 196 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== 197 | 198 | has-symbols@^1.0.2, has-symbols@^1.0.3: 199 | version "1.0.3" 200 | resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 201 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 202 | 203 | has-tostringtag@^1.0.0: 204 | version "1.0.0" 205 | resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" 206 | integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== 207 | dependencies: 208 | has-symbols "^1.0.2" 209 | 210 | has@^1.0.3: 211 | version "1.0.3" 212 | resolved "https://registry.npmmirror.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 213 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 214 | dependencies: 215 | function-bind "^1.1.1" 216 | 217 | http-assert@^1.3.0: 218 | version "1.5.0" 219 | resolved "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" 220 | integrity sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w== 221 | dependencies: 222 | deep-equal "~1.0.1" 223 | http-errors "~1.8.0" 224 | 225 | http-errors@2.0.0: 226 | version "2.0.0" 227 | resolved "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 228 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 229 | dependencies: 230 | depd "2.0.0" 231 | inherits "2.0.4" 232 | setprototypeof "1.2.0" 233 | statuses "2.0.1" 234 | toidentifier "1.0.1" 235 | 236 | http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.8.0: 237 | version "1.8.1" 238 | resolved "https://registry.npmmirror.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" 239 | integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== 240 | dependencies: 241 | depd "~1.1.2" 242 | inherits "2.0.4" 243 | setprototypeof "1.2.0" 244 | statuses ">= 1.5.0 < 2" 245 | toidentifier "1.0.1" 246 | 247 | iconv-lite@0.4.24: 248 | version "0.4.24" 249 | resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 250 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 251 | dependencies: 252 | safer-buffer ">= 2.1.2 < 3" 253 | 254 | inflation@^2.0.0: 255 | version "2.0.0" 256 | resolved "https://registry.npmmirror.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" 257 | integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw== 258 | 259 | inherits@2.0.4: 260 | version "2.0.4" 261 | resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 262 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 263 | 264 | is-generator-function@^1.0.7: 265 | version "1.0.10" 266 | resolved "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" 267 | integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== 268 | dependencies: 269 | has-tostringtag "^1.0.0" 270 | 271 | keygrip@~1.1.0: 272 | version "1.1.0" 273 | resolved "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" 274 | integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== 275 | dependencies: 276 | tsscmp "1.0.6" 277 | 278 | koa-body@^4.2.0: 279 | version "4.2.0" 280 | resolved "https://registry.npmmirror.com/koa-body/-/koa-body-4.2.0.tgz#37229208b820761aca5822d14c5fc55cee31b26f" 281 | integrity sha512-wdGu7b9amk4Fnk/ytH8GuWwfs4fsB5iNkY8kZPpgQVb04QZSv85T0M8reb+cJmvLE8cjPYvBzRikD3s6qz8OoA== 282 | dependencies: 283 | "@types/formidable" "^1.0.31" 284 | co-body "^5.1.1" 285 | formidable "^1.1.1" 286 | 287 | koa-compose@^4.1.0: 288 | version "4.1.0" 289 | resolved "https://registry.npmmirror.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" 290 | integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== 291 | 292 | koa-convert@^2.0.0: 293 | version "2.0.0" 294 | resolved "https://registry.npmmirror.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" 295 | integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA== 296 | dependencies: 297 | co "^4.6.0" 298 | koa-compose "^4.1.0" 299 | 300 | koa@^2.13.4: 301 | version "2.14.2" 302 | resolved "https://registry.npmmirror.com/koa/-/koa-2.14.2.tgz#a57f925c03931c2b4d94b19d2ebf76d3244863fc" 303 | integrity sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g== 304 | dependencies: 305 | accepts "^1.3.5" 306 | cache-content-type "^1.0.0" 307 | content-disposition "~0.5.2" 308 | content-type "^1.0.4" 309 | cookies "~0.8.0" 310 | debug "^4.3.2" 311 | delegates "^1.0.0" 312 | depd "^2.0.0" 313 | destroy "^1.0.4" 314 | encodeurl "^1.0.2" 315 | escape-html "^1.0.3" 316 | fresh "~0.5.2" 317 | http-assert "^1.3.0" 318 | http-errors "^1.6.3" 319 | is-generator-function "^1.0.7" 320 | koa-compose "^4.1.0" 321 | koa-convert "^2.0.0" 322 | on-finished "^2.3.0" 323 | only "~0.0.2" 324 | parseurl "^1.3.2" 325 | statuses "^1.5.0" 326 | type-is "^1.6.16" 327 | vary "^1.1.2" 328 | 329 | long-timeout@0.1.1: 330 | version "0.1.1" 331 | resolved "https://registry.npmmirror.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" 332 | integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== 333 | 334 | luxon@^3.2.1: 335 | version "3.4.3" 336 | resolved "https://registry.npmmirror.com/luxon/-/luxon-3.4.3.tgz#8ddf0358a9492267ffec6a13675fbaab5551315d" 337 | integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg== 338 | 339 | media-typer@0.3.0: 340 | version "0.3.0" 341 | resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 342 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 343 | 344 | methods@^1.1.2: 345 | version "1.1.2" 346 | resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 347 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 348 | 349 | mime-db@1.52.0: 350 | version "1.52.0" 351 | resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 352 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 353 | 354 | mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34: 355 | version "2.1.35" 356 | resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 357 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 358 | dependencies: 359 | mime-db "1.52.0" 360 | 361 | moment@^2.29.1: 362 | version "2.29.4" 363 | resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" 364 | integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== 365 | 366 | ms@2.1.2: 367 | version "2.1.2" 368 | resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 369 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 370 | 371 | negotiator@0.6.3: 372 | version "0.6.3" 373 | resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 374 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 375 | 376 | node-schedule@^2.1.0: 377 | version "2.1.1" 378 | resolved "https://registry.npmmirror.com/node-schedule/-/node-schedule-2.1.1.tgz#6958b2c5af8834954f69bb0a7a97c62b97185de3" 379 | integrity sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ== 380 | dependencies: 381 | cron-parser "^4.2.0" 382 | long-timeout "0.1.1" 383 | sorted-array-functions "^1.3.0" 384 | 385 | object-inspect@^1.9.0: 386 | version "1.12.3" 387 | resolved "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" 388 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== 389 | 390 | on-finished@^2.3.0: 391 | version "2.4.1" 392 | resolved "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 393 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 394 | dependencies: 395 | ee-first "1.1.1" 396 | 397 | only@~0.0.2: 398 | version "0.0.2" 399 | resolved "https://registry.npmmirror.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" 400 | integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== 401 | 402 | parseurl@^1.3.2: 403 | version "1.3.3" 404 | resolved "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 405 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 406 | 407 | path-to-regexp@^6.1.0: 408 | version "6.2.1" 409 | resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" 410 | integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== 411 | 412 | qs@^6.4.0: 413 | version "6.11.2" 414 | resolved "https://registry.npmmirror.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" 415 | integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== 416 | dependencies: 417 | side-channel "^1.0.4" 418 | 419 | raw-body@^2.2.0: 420 | version "2.5.2" 421 | resolved "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" 422 | integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== 423 | dependencies: 424 | bytes "3.1.2" 425 | http-errors "2.0.0" 426 | iconv-lite "0.4.24" 427 | unpipe "1.0.0" 428 | 429 | safe-buffer@5.2.1: 430 | version "5.2.1" 431 | resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 432 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 433 | 434 | "safer-buffer@>= 2.1.2 < 3": 435 | version "2.1.2" 436 | resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 437 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 438 | 439 | setprototypeof@1.2.0: 440 | version "1.2.0" 441 | resolved "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 442 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 443 | 444 | side-channel@^1.0.4: 445 | version "1.0.4" 446 | resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 447 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 448 | dependencies: 449 | call-bind "^1.0.0" 450 | get-intrinsic "^1.0.2" 451 | object-inspect "^1.9.0" 452 | 453 | sorted-array-functions@^1.3.0: 454 | version "1.3.0" 455 | resolved "https://registry.npmmirror.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" 456 | integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== 457 | 458 | statuses@2.0.1: 459 | version "2.0.1" 460 | resolved "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 461 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 462 | 463 | "statuses@>= 1.5.0 < 2", statuses@^1.5.0: 464 | version "1.5.0" 465 | resolved "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 466 | integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== 467 | 468 | toidentifier@1.0.1: 469 | version "1.0.1" 470 | resolved "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 471 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 472 | 473 | tsscmp@1.0.6: 474 | version "1.0.6" 475 | resolved "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" 476 | integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== 477 | 478 | type-is@^1.6.14, type-is@^1.6.16: 479 | version "1.6.18" 480 | resolved "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 481 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 482 | dependencies: 483 | media-typer "0.3.0" 484 | mime-types "~2.1.24" 485 | 486 | unpipe@1.0.0: 487 | version "1.0.0" 488 | resolved "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 489 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 490 | 491 | vary@^1.1.2: 492 | version "1.1.2" 493 | resolved "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 494 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 495 | 496 | yaml@^2.2.1: 497 | version "2.3.2" 498 | resolved "https://registry.npmmirror.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" 499 | integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== 500 | 501 | ylru@^1.2.0: 502 | version "1.3.2" 503 | resolved "https://registry.npmmirror.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" 504 | integrity sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA== 505 | --------------------------------------------------------------------------------