From e35712820fc4c023b9268a82c8138d0ae32102a9 Mon Sep 17 00:00:00 2001 From: abearixong Date: Fri, 14 Nov 2025 20:43:59 +0800 Subject: [PATCH] update --- .env.example | 0 .gitignore | 6 +- agent/app.ts | 3 + agent/ddns/cloudflare/index.ts | 37 +++ agent/ddns/volcengine/index.ts | 12 + agent/ddns/volcengine/sign.ts | 250 ++++++++++++++++++ agent/index.ts | 14 + agent/ip.ts | 41 +++ bun.lock => ddns-agent-slide/bun.lock | 0 .../components}/Counter.vue | 0 ddns-agent-slide/package.json | 18 ++ {pages => ddns-agent-slide/pages}/demos.md | 0 slides.md => ddns-agent-slide/slides.md | 0 .../snippets}/external.ts | 0 package.json | 28 +- pnpm-lock.yaml | 211 +++++++++++++++ 16 files changed, 606 insertions(+), 14 deletions(-) create mode 100644 .env.example create mode 100644 agent/app.ts create mode 100644 agent/ddns/cloudflare/index.ts create mode 100644 agent/ddns/volcengine/index.ts create mode 100644 agent/ddns/volcengine/sign.ts create mode 100644 agent/index.ts create mode 100644 agent/ip.ts rename bun.lock => ddns-agent-slide/bun.lock (100%) rename {components => ddns-agent-slide/components}/Counter.vue (100%) create mode 100644 ddns-agent-slide/package.json rename {pages => ddns-agent-slide/pages}/demos.md (100%) rename slides.md => ddns-agent-slide/slides.md (100%) rename {snippets => ddns-agent-slide/snippets}/external.ts (100%) create mode 100644 pnpm-lock.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index f005dfa..b1a3dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ dist .remote-assets components.d.ts -.slides.md.swp \ No newline at end of file +.slides.md.swp + +.env + +!.env*example \ No newline at end of file diff --git a/agent/app.ts b/agent/app.ts new file mode 100644 index 0000000..12315cf --- /dev/null +++ b/agent/app.ts @@ -0,0 +1,3 @@ +import { App } from '@kevisual/router' + +export const app = new App(); \ No newline at end of file diff --git a/agent/ddns/cloudflare/index.ts b/agent/ddns/cloudflare/index.ts new file mode 100644 index 0000000..a1c7dff --- /dev/null +++ b/agent/ddns/cloudflare/index.ts @@ -0,0 +1,37 @@ +interface DnsUpdate { + zone_id: string; + record_id: string; + domain: string; + new_ip: string; + api_token: string; + type?: string; // 'A' or 'AAAA' +} +export class CloudflareDDNS { + makeHeader(api_token: string) { + return { + 'Authorization': `Bearer ${api_token}`, + 'Content-Type': 'application/json', + }; + } + async updateRecord(data: DnsUpdate) { + const { zone_id, record_id, domain, type ,new_ip, api_token } = data; + const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`; + const body = { + "type": type || 'A', + "name": domain, + "content": new_ip, + "ttl": 0, + "proxied": false, + } + const response = await fetch(url, { + method: 'PUT', + headers: this.makeHeader(api_token), + body: JSON.stringify(body), + }); + const result = await response.json(); + if(!result.success) { + throw new Error(`更新失败: ${JSON.stringify(result.errors)}`); + } + return result; + } +} \ No newline at end of file diff --git a/agent/ddns/volcengine/index.ts b/agent/ddns/volcengine/index.ts new file mode 100644 index 0000000..9967906 --- /dev/null +++ b/agent/ddns/volcengine/index.ts @@ -0,0 +1,12 @@ +// 文档地址 https://www.volcengine.com/docs/6758/155086 + +// 记录管理 +// 1. 获取域名记录列表 ListRecords +// 2. 获取单个域名记录 QueryRecord +// 3. 添加域名记录 CreateRecord +// 4. 修改域名记录 UpdateRecord +// 5. 删除域名记录 DeleteRecord +export class VolcengineDDNS { + baseURL = 'https://dns.volcengineapi.com'; + version = '2018-08-01' +} \ No newline at end of file diff --git a/agent/ddns/volcengine/sign.ts b/agent/ddns/volcengine/sign.ts new file mode 100644 index 0000000..274401a --- /dev/null +++ b/agent/ddns/volcengine/sign.ts @@ -0,0 +1,250 @@ +const Service = "DNS"; +const Version = "2018-08-01"; +const Region = "cn-north-1"; +const Host = "dns.volcengineapi.com"; + +interface Credential { + access_key_id: string; + secret_access_key: string; + service: string; + region: string; +} + +interface RequestParam { + body: string; + host: string; + path: string; + method: string; + content_type: string; + date: Date; + query: Record; +} + +interface SignResult { + Host: string; + 'X-Content-Sha256': string; + 'X-Date': string; + 'Content-Type': string; + Authorization?: string; +} + +/** + * 字符串转ArrayBuffer + */ +function stringToArrayBuffer(str: string): ArrayBuffer { + const encoder = new TextEncoder(); + return encoder.encode(str).buffer; +} + +/** + * ArrayBuffer转十六进制字符串 + */ +function arrayBufferToHex(buffer: ArrayBuffer): string { + const hashArray = Array.from(new Uint8Array(buffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * URL编码函数,类似Python的quote函数 + */ +function encodeURIComponentSafe(str: string): string { + return encodeURIComponent(str) + .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()) + .replace(/%20/g, '%20'); +} + +/** + * 规范化查询参数 + */ +function normQuery(params: Record): string { + let query = ""; + const sortedKeys = Object.keys(params).sort(); + + for (const key of sortedKeys) { + if (Array.isArray(params[key])) { + for (const k of params[key]) { + query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(k) + "&"; + } + } else { + query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(params[key]) + "&"; + } + } + + query = query.slice(0, -1); + return query.replace(/\+/g, "%20"); +} + +/** + * HMAC-SHA256 加密 (使用Web Crypto API) + */ +async function hmacSha256(key: ArrayBuffer, content: string): Promise { + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + return crypto.subtle.sign('HMAC', cryptoKey, stringToArrayBuffer(content)); +} + +/** + * SHA256 哈希算法 (使用Web Crypto API) + */ +async function hashSha256(content: string): Promise { + const hashBuffer = await crypto.subtle.digest('SHA-256', stringToArrayBuffer(content)); + return arrayBufferToHex(hashBuffer); +} + +/** + * 获取签名 + */ +export async function getSignature( + method: string, + query: Record, + header: Record, + ak: string, + sk: string, + action: string, + body: any +): Promise { + const credential: Credential = { + access_key_id: ak, + secret_access_key: sk, + service: Service, + region: Region, + }; + + const fullQuery = { Action: action, Version: Version, ...query }; + const sortedQuery: Record = {}; + Object.keys(fullQuery).sort().forEach(key => { + sortedQuery[key] = (fullQuery as any)[key]; + }); + + const requestParam: RequestParam = { + body: "", + host: Host, + path: "/", + method: method, + content_type: "application/json", + date: new Date(), + query: sortedQuery, + }; + + if (method === "POST") { + requestParam.body = JSON.stringify(body); + } + + const xDate = requestParam.date.toISOString().replace(/[:\-]|\.\d{3}/g, '').slice(0, -1) + 'Z'; + const shortXDate = xDate.slice(0, 8); + const xContentSha256 = await hashSha256(requestParam.body); + + const signResult: SignResult = { + Host: requestParam.host, + 'X-Content-Sha256': xContentSha256, + 'X-Date': xDate, + 'Content-Type': requestParam.content_type, + }; + + const signedHeadersStr = ["content-type", "host", "x-content-sha256", "x-date"].join(";"); + + const canonicalRequestStr = [ + requestParam.method, + requestParam.path, + normQuery(requestParam.query), + [ + "content-type:" + requestParam.content_type, + "host:" + requestParam.host, + "x-content-sha256:" + xContentSha256, + "x-date:" + xDate, + ].join("\n"), + "", + signedHeadersStr, + xContentSha256, + ].join("\n"); + + const hashedCanonicalRequest = await hashSha256(canonicalRequestStr); + const credentialScope = [shortXDate, credential.region, credential.service, "request"].join("/"); + const stringToSign = ["HMAC-SHA256", xDate, credentialScope, hashedCanonicalRequest].join("\n"); + + const secretKeyBuffer = stringToArrayBuffer(credential.secret_access_key); + const kDate = await hmacSha256(secretKeyBuffer, shortXDate); + const kRegion = await hmacSha256(kDate, credential.region); + const kService = await hmacSha256(kRegion, credential.service); + const kSigning = await hmacSha256(kService, "request"); + const signatureBuffer = await hmacSha256(kSigning, stringToSign); + const signature = arrayBufferToHex(signatureBuffer); + + signResult.Authorization = `HMAC-SHA256 Credential=${credential.access_key_id}/${credentialScope}, SignedHeaders=${signedHeadersStr}, Signature=${signature}`; + + return signResult; +} + +/** + * 发送API请求 + */ +export async function request( + method: string, + query: Record, + header: Record, + ak: string, + sk: string, + action: string, + body: any +): Promise { + const signResult = await getSignature(method, query, header, ak, sk, action, body); + const finalHeader = { ...header, ...signResult }; + + const requestParam: RequestParam = { + body: method === "POST" ? JSON.stringify(body) : "", + host: Host, + path: "/", + method: method, + content_type: "application/json", + date: new Date(), + query: { Action: action, Version: Version, ...query }, + }; + + const url = new URL(`https://${requestParam.host}${requestParam.path}`); + Object.keys(requestParam.query).forEach(key => { + url.searchParams.append(key, requestParam.query[key]); + }); + + const fetchOptions: RequestInit = { + method: method, + headers: finalHeader, + }; + + if (method === "POST") { + fetchOptions.body = requestParam.body; + } + + try { + const response = await fetch(url.toString(), fetchOptions); + return await response.json(); + } catch (error) { + throw new Error(`Request failed: ${error}`); + } +} + +/** + * 使用示例 + */ +export async function example(ak: string, sk: string) { + try { + // POST 请求示例 - 调用 UpdateZone API + const requestBody = { + ZID: 100, + Remark: "example", + }; + const updateZoneResult = await request("POST", {}, {}, ak, sk, "UpdateZone", requestBody); + console.log(updateZoneResult); + + // GET 请求示例 - 调用 CheckZone API + const requestQuery = { ZoneName: "example.com" }; + const checkZoneResult = await request("GET", requestQuery, {}, ak, sk, "CheckZone", {}); + console.log(checkZoneResult); + } catch (error) { + console.error('API request failed:', error); + } +} diff --git a/agent/index.ts b/agent/index.ts new file mode 100644 index 0000000..719ec59 --- /dev/null +++ b/agent/index.ts @@ -0,0 +1,14 @@ +import {app} from './app.ts'; + +import './ip' + +// app.listen(8080); + +app.call({ + path: 'ip', + key: 'v6' +}).then(res => { + console.log('IPv4 Address:', res); +}) + +export { app }; \ No newline at end of file diff --git a/agent/ip.ts b/agent/ip.ts new file mode 100644 index 0000000..7266283 --- /dev/null +++ b/agent/ip.ts @@ -0,0 +1,41 @@ +const baseURLv4 = 'https://4.ipw.cn/'; +const baseURLv6 = 'https://6.ipw.cn/'; + + +import { app } from './app.ts'; + + +app.route({ + path: 'ip', + key: 'v4', + description: '获取当前公网IPv4地址', +}).define(async (ctx) => { + const response = await fetch(baseURLv4); + const ip = (await response.text()).trim(); + if(!isIpv4(ip)) { + ctx.throw?.('获取地址失败'); + } + ctx.body = { ip }; +}).addTo(app); + +app.route({ + path: 'ip', + key: 'v6', + description: '获取当前公网IPv6地址', +}).define(async (ctx) => { + const response = await fetch(baseURLv6); + const ip = (await response.text()).trim(); + if(!isIpv6(ip)) { + ctx.throw?.('获取地址失败'); + } + ctx.body = { ip }; +}).addTo(app); + + +export const isIpv6 = (ip: string): boolean => { + return ip.includes(':'); +} + +export const isIpv4 = (ip: string): boolean => { + return ip.split('.').length === 4; +} \ No newline at end of file diff --git a/bun.lock b/ddns-agent-slide/bun.lock similarity index 100% rename from bun.lock rename to ddns-agent-slide/bun.lock diff --git a/components/Counter.vue b/ddns-agent-slide/components/Counter.vue similarity index 100% rename from components/Counter.vue rename to ddns-agent-slide/components/Counter.vue diff --git a/ddns-agent-slide/package.json b/ddns-agent-slide/package.json new file mode 100644 index 0000000..09dd76d --- /dev/null +++ b/ddns-agent-slide/package.json @@ -0,0 +1,18 @@ +{ + "name": "@kevisual/slidev-template", + "type": "module", + "basename": "/root/slidev-template", + "version": "0.0.1", + "scripts": { + "build": "slidev build --base /root/slidev-template/", + "dev": "slidev --open", + "pub": "ev deploy ./dist -k slidev-template -v 0.0.1 -u", + "export": "slidev export" + }, + "dependencies": { + "@slidev/cli": "^52.8.0", + "@slidev/theme-default": "latest", + "@slidev/theme-seriph": "latest", + "vue": "^3.5.24" + } +} \ No newline at end of file diff --git a/pages/demos.md b/ddns-agent-slide/pages/demos.md similarity index 100% rename from pages/demos.md rename to ddns-agent-slide/pages/demos.md diff --git a/slides.md b/ddns-agent-slide/slides.md similarity index 100% rename from slides.md rename to ddns-agent-slide/slides.md diff --git a/snippets/external.ts b/ddns-agent-slide/snippets/external.ts similarity index 100% rename from snippets/external.ts rename to ddns-agent-slide/snippets/external.ts diff --git a/package.json b/package.json index 09dd76d..3b10c5e 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,20 @@ { - "name": "@kevisual/slidev-template", - "type": "module", - "basename": "/root/slidev-template", - "version": "0.0.1", + "name": "@kevisual/ddns-agent", + "version": "1.0.0", + "description": "", + "main": "index.js", "scripts": { - "build": "slidev build --base /root/slidev-template/", - "dev": "slidev --open", - "pub": "ev deploy ./dist -k slidev-template -v 0.0.1 -u", - "export": "slidev export" + "test": "echo \"Error: no test specified\" && exit 1" }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.19.0", "dependencies": { - "@slidev/cli": "^52.8.0", - "@slidev/theme-default": "latest", - "@slidev/theme-seriph": "latest", - "vue": "^3.5.24" + "@kevisual/router": "^0.0.30", + "crypto-js": "^4.2.0" + }, + "devDependencies": { + "@types/node": "^24.10.1" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0cf9d38 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,211 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@kevisual/router': + specifier: ^0.0.30 + version: 0.0.30 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + +packages: + + '@kevisual/router@0.0.30': + resolution: {integrity: sha512-/mBo7aZFWjT4QfHkI5HPXfdgSwZzt3mAVei7dcNSBTPe9KQSoYKZ8BTq9VTUj3XE0sI6o1bZjlLYvinpVnZilw==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + selfsigned@3.0.1: + resolution: {integrity: sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==} + engines: {node: '>=10'} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + +snapshots: + + '@kevisual/router@0.0.30': + dependencies: + path-to-regexp: 8.3.0 + selfsigned: 3.0.1 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + crypto-js@4.2.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + fresh@2.0.0: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + inherits@2.0.4: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + node-forge@1.3.1: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + path-to-regexp@8.3.0: {} + + range-parser@1.2.1: {} + + selfsigned@3.0.1: + dependencies: + node-forge: 1.3.1 + + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + undici-types@7.16.0: {}